mirror of
https://github.com/int128/kubelogin.git
synced 2026-05-09 01:16:35 +00:00
157 lines
5.0 KiB
Go
157 lines
5.0 KiB
Go
// Package handler provides HTTP handlers for the OpenID Connect Provider.
|
|
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/int128/kubelogin/integration_test/oidcserver/service"
|
|
)
|
|
|
|
func Register(t *testing.T, mux *http.ServeMux, provider service.Provider) {
|
|
h := &Handlers{t, provider}
|
|
mux.HandleFunc("GET /.well-known/openid-configuration", h.Discovery)
|
|
mux.HandleFunc("GET /certs", h.GetCertificates)
|
|
mux.HandleFunc("GET /auth", h.AuthenticateCode)
|
|
mux.HandleFunc("POST /token", h.Exchange)
|
|
}
|
|
|
|
// Handlers provides HTTP handlers for the OpenID Connect Provider.
|
|
// You need to implement the Provider interface.
|
|
// Note that this skips some security checks and is only for testing.
|
|
type Handlers struct {
|
|
t *testing.T
|
|
provider service.Provider
|
|
}
|
|
|
|
func (h *Handlers) handleError(w http.ResponseWriter, r *http.Request, f func() error) {
|
|
err := f()
|
|
if err == nil {
|
|
return
|
|
}
|
|
if errResp := new(service.ErrorResponse); errors.As(err, &errResp) {
|
|
h.t.Logf("400 %s %s: %s", r.Method, r.RequestURI, err)
|
|
w.Header().Add("Content-Type", "application/json")
|
|
w.WriteHeader(400)
|
|
e := json.NewEncoder(w)
|
|
if err := e.Encode(errResp); err != nil {
|
|
h.t.Errorf("idp/handler: could not write the response: %s", err)
|
|
}
|
|
return
|
|
}
|
|
h.t.Logf("500 %s %s: %s", r.Method, r.RequestURI, err)
|
|
http.Error(w, err.Error(), 500)
|
|
}
|
|
|
|
func (h *Handlers) Discovery(w http.ResponseWriter, r *http.Request) {
|
|
h.handleError(w, r, func() error {
|
|
discoveryResponse := h.provider.Discovery()
|
|
w.Header().Add("Content-Type", "application/json")
|
|
e := json.NewEncoder(w)
|
|
if err := e.Encode(discoveryResponse); err != nil {
|
|
return fmt.Errorf("could not render json: %w", err)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (h *Handlers) GetCertificates(w http.ResponseWriter, r *http.Request) {
|
|
h.handleError(w, r, func() error {
|
|
certificatesResponse := h.provider.GetCertificates()
|
|
w.Header().Add("Content-Type", "application/json")
|
|
e := json.NewEncoder(w)
|
|
if err := e.Encode(certificatesResponse); err != nil {
|
|
return fmt.Errorf("could not render json: %w", err)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (h *Handlers) AuthenticateCode(w http.ResponseWriter, r *http.Request) {
|
|
h.handleError(w, r, func() error {
|
|
q := r.URL.Query()
|
|
redirectURI, state := q.Get("redirect_uri"), q.Get("state")
|
|
code, err := h.provider.AuthenticateCode(service.AuthenticationRequest{
|
|
RedirectURI: redirectURI,
|
|
State: state,
|
|
Scope: q.Get("scope"),
|
|
Nonce: q.Get("nonce"),
|
|
CodeChallenge: q.Get("code_challenge"),
|
|
CodeChallengeMethod: q.Get("code_challenge_method"),
|
|
RawQuery: q,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("authentication error: %w", err)
|
|
}
|
|
redirectTo, err := url.Parse(redirectURI)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid redirect_uri: %w", err)
|
|
}
|
|
redirectTo.RawQuery = url.Values{"state": {state}, "code": {code}}.Encode()
|
|
http.Redirect(w, r, redirectTo.String(), http.StatusFound)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (h *Handlers) Exchange(w http.ResponseWriter, r *http.Request) {
|
|
h.handleError(w, r, func() error {
|
|
if err := r.ParseForm(); err != nil {
|
|
return fmt.Errorf("could not parse the form: %w", err)
|
|
}
|
|
grantType := r.Form.Get("grant_type")
|
|
switch grantType {
|
|
case "authorization_code":
|
|
tokenResponse, err := h.provider.Exchange(service.TokenRequest{
|
|
Code: r.Form.Get("code"),
|
|
CodeVerifier: r.Form.Get("code_verifier"),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("token request error: %w", err)
|
|
}
|
|
w.Header().Add("Content-Type", "application/json")
|
|
e := json.NewEncoder(w)
|
|
if err := e.Encode(tokenResponse); err != nil {
|
|
return fmt.Errorf("could not render json: %w", err)
|
|
}
|
|
case "password":
|
|
// 4.3. Resource Owner Password Credentials Grant
|
|
// https://tools.ietf.org/html/rfc6749#section-4.3
|
|
username, password, scope := r.Form.Get("username"), r.Form.Get("password"), r.Form.Get("scope")
|
|
tokenResponse, err := h.provider.AuthenticatePassword(username, password, scope)
|
|
if err != nil {
|
|
return fmt.Errorf("authentication error: %w", err)
|
|
}
|
|
w.Header().Add("Content-Type", "application/json")
|
|
e := json.NewEncoder(w)
|
|
if err := e.Encode(tokenResponse); err != nil {
|
|
return fmt.Errorf("could not render json: %w", err)
|
|
}
|
|
case "refresh_token":
|
|
// 12.1. Refresh Request
|
|
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken
|
|
refreshToken := r.Form.Get("refresh_token")
|
|
tokenResponse, err := h.provider.Refresh(refreshToken)
|
|
if err != nil {
|
|
return fmt.Errorf("token refresh error: %w", err)
|
|
}
|
|
w.Header().Add("Content-Type", "application/json")
|
|
e := json.NewEncoder(w)
|
|
if err := e.Encode(tokenResponse); err != nil {
|
|
return fmt.Errorf("could not render json: %w", err)
|
|
}
|
|
default:
|
|
// 5.2. Error Response
|
|
// https://tools.ietf.org/html/rfc6749#section-5.2
|
|
return &service.ErrorResponse{
|
|
Code: "invalid_grant",
|
|
Description: fmt.Sprintf("unknown grant_type %s", grantType),
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|