loginurl as our own implementation

This commit is contained in:
Kim Tore Jensen
2021-08-19 13:05:39 +02:00
parent 60ce40e404
commit 4da8e5263f
3 changed files with 121 additions and 54 deletions

1
go.mod
View File

@@ -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
)

View File

@@ -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)
}

32
pkg/router/router_test.go Normal file
View File

@@ -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)
}