mirror of
https://github.com/nais/wonderwall.git
synced 2026-05-14 20:36:40 +00:00
loginurl as our own implementation
This commit is contained in:
1
go.mod
1
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
|
||||
)
|
||||
|
||||
@@ -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
32
pkg/router/router_test.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user