From 4da8e5263f2241ff846e6d965487adfeef9b6284 Mon Sep 17 00:00:00 2001 From: Kim Tore Jensen Date: Thu, 19 Aug 2021 13:05:39 +0200 Subject: [PATCH] loginurl as our own implementation --- go.mod | 1 + pkg/router/router.go | 142 +++++++++++++++++++++++--------------- pkg/router/router_test.go | 32 +++++++++ 3 files changed, 121 insertions(+), 54 deletions(-) create mode 100644 pkg/router/router_test.go diff --git a/go.mod b/go.mod index b558435..b646250 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/sirupsen/logrus v1.8.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.8.1 + github.com/stretchr/testify v1.7.0 // indirect golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 gopkg.in/square/go-jose.v2 v2.6.0 ) diff --git a/pkg/router/router.go b/pkg/router/router.go index c65812f..105dc56 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -2,16 +2,20 @@ package router import ( "crypto/rand" - "encoding/hex" + "crypto/sha256" + "encoding/base64" "encoding/json" + "fmt" + "gopkg.in/square/go-jose.v2" + "io" "net/http" + "net/url" + "time" "github.com/caos/oidc/pkg/client/rp" "github.com/caos/oidc/pkg/oidc" "github.com/go-chi/chi" - "github.com/google/uuid" "github.com/nais/wonderwall/pkg/config" - "golang.org/x/oauth2" ) const ( @@ -26,67 +30,96 @@ type Handler struct { RelyingParty rp.RelyingParty } +type loginParams struct { + cookies []*http.Cookie + state []byte + codeVerifier []byte + url string +} + +func (h *Handler) LoginURL() (*loginParams, error) { + codeVerifier := make([]byte, 32) + nonce := make([]byte, 32) + state := make([]byte, 32) + + var err error + + _, err = io.ReadFull(rand.Reader, state) + if err != nil { + return nil, fmt.Errorf("failed to create state: %w", err) + } + + _, err = io.ReadFull(rand.Reader, nonce) + if err != nil { + return nil, fmt.Errorf("failed to create nonce: %w", err) + } + + _, err = io.ReadFull(rand.Reader, codeVerifier) + if err != nil { + return nil, fmt.Errorf("failed to create code verifier: %w", err) + } + + hasher := sha256.New() + codeVerifierHash := hasher.Sum(nil) + + u, err := url.Parse(h.Config.WellKnown.AuthorizationEndpoint) + if err != nil { + return nil, err + } + v := u.Query() + v.Add("response_type", "code") + v.Add("client_id", h.Config.ClientID) + v.Add("redirect_uri", h.Config.RedirectURI) + v.Add("scope", "openid") + v.Add("state", base64.RawURLEncoding.EncodeToString(state)) + v.Add("nonce", base64.RawURLEncoding.EncodeToString(nonce)) + v.Add("acr_values", h.Config.SecurityLevel) + v.Add("response_mode", "query") + v.Add("ui_locales", h.Config.Locale) + v.Add("code_challenge", base64.RawURLEncoding.EncodeToString(codeVerifierHash)) + v.Add("code_challenge_method", "S256") + u.RawQuery = v.Encode() + + return &loginParams{ + state: state, + codeVerifier: codeVerifier, + url: u.String(), + }, nil +} + func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { - opts := make([]rp.AuthURLOpt, 0) - randomUUID, err := uuid.NewRandom() + params, err := h.LoginURL() if err != nil { - http.Error(w, "failed to create state: "+err.Error(), http.StatusUnauthorized) - return - } - state := randomUUID.String() - - randomUUID2, err := uuid.NewRandom() - if err != nil { - http.Error(w, "failed to create nonce: "+err.Error(), http.StatusUnauthorized) - return - } - nonce := randomUUID2.String() - - if err := h.RelyingParty.CookieHandler().SetCookie(w, nonceParam, nonce); err != nil { - http.Error(w, "failed to create nonce cookie: "+err.Error(), http.StatusUnauthorized) + w.WriteHeader(http.StatusInternalServerError) return } - if err := h.RelyingParty.CookieHandler().SetCookie(w, stateParam, state); err != nil { - http.Error(w, "failed to create state cookie: "+err.Error(), http.StatusUnauthorized) - return - } - - codeBytes := make([]byte, 32) - read, err := rand.Read(codeBytes) - - if err != nil { - http.Error(w, "failed to create code: "+err.Error(), http.StatusUnauthorized) - return - } else if read != 32 { - http.Error(w, "failed to create code: could not read 32 bytes", http.StatusUnauthorized) - return - } - - codeVerifier := hex.EncodeToString(codeBytes) - - if err := h.RelyingParty.CookieHandler().SetCookie(w, pkceCode, codeVerifier); err != nil { - http.Error(w, "failed to create code challenge: "+err.Error(), http.StatusUnauthorized) - return - } - codeChallenge := oidc.NewSHACodeChallenge(codeVerifier) - - opts = append(opts, rp.WithCodeChallenge(codeChallenge)) - opts = append(opts, func() []oauth2.AuthCodeOption { - return []oauth2.AuthCodeOption{ - oauth2.SetAuthURLParam("acr_values", h.Config.SecurityLevel), - oauth2.SetAuthURLParam("ui_locales", h.Config.Locale), - oauth2.SetAuthURLParam("response_mode", "query"), - oauth2.SetAuthURLParam("nonce", nonce), - } + http.SetCookie(w, &http.Cookie{ + Name: "state", + Value: string(params.state), + Expires: time.Now().Add(10 * time.Minute), + Secure: true, + SameSite: http.SameSiteLaxMode, + }) + http.SetCookie(w, &http.Cookie{ + Name: "code_verifier", + Value: string(params.codeVerifier), + Expires: time.Now().Add(10 * time.Minute), + Secure: true, + SameSite: http.SameSiteLaxMode, }) - url := rp.AuthURL(state, h.RelyingParty, opts...) - - http.Redirect(w, r, url, http.StatusFound) + http.Redirect(w, r, params.url, http.StatusTemporaryRedirect) } func (h *Handler) Callback(w http.ResponseWriter, r *http.Request) { + key := &jose.JSONWebKey{} + err := json.Unmarshal([]byte(h.Config.ClientJWK), key) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + marshalToken := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty) { data, err := json.Marshal(tokens) if err != nil { @@ -95,6 +128,7 @@ func (h *Handler) Callback(w http.ResponseWriter, r *http.Request) { } w.Write(data) } + rp.CodeExchangeHandler(marshalToken, h.RelyingParty)(w, r) } diff --git a/pkg/router/router_test.go b/pkg/router/router_test.go new file mode 100644 index 0000000..0207cb1 --- /dev/null +++ b/pkg/router/router_test.go @@ -0,0 +1,32 @@ +package router_test + +import ( + "encoding/json" + "github.com/nais/wonderwall/pkg/config" + "github.com/nais/wonderwall/pkg/router" + "github.com/stretchr/testify/assert" + "gopkg.in/square/go-jose.v2" + "testing" +) + +func TestJWK(t *testing.T) { + key := &jose.JSONWebKey{} + _ = json.Unmarshal([]byte(``), key) +} + +func TestLoginURL(t *testing.T) { + handler := &router.Handler{ + Config: config.IDPorten{ + ClientID: "clientid", + RedirectURI: "http://localhost/redirect", + WellKnown: config.IDPortenWellKnown{ + AuthorizationEndpoint: "http://localhost:1234/authorize", + }, + Locale: "nb", + SecurityLevel: "Level4", + }, + } + params, err := handler.LoginURL() + assert.NoError(t, err) + t.Logf("%+v", params) +}