diff options
| author | mo khan <mo@mokhan.ca> | 2025-05-11 21:12:57 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-05-11 21:12:57 -0600 |
| commit | 60440f90dca28e99a31dd328c5f6d5dc0f9b6a2e (patch) | |
| tree | 2f54adf55086516f162f0a55a5347e6b25f7f176 /vendor/github.com/oauth2-proxy/mockoidc | |
| parent | 05ca9b8d3a9c7203a3a3b590beaa400900bd9007 (diff) | |
chore: vendor go dependencies
Diffstat (limited to 'vendor/github.com/oauth2-proxy/mockoidc')
| -rw-r--r-- | vendor/github.com/oauth2-proxy/mockoidc/.gitignore | 15 | ||||
| -rw-r--r-- | vendor/github.com/oauth2-proxy/mockoidc/.tool-versions | 1 | ||||
| -rw-r--r-- | vendor/github.com/oauth2-proxy/mockoidc/LICENSE | 21 | ||||
| -rw-r--r-- | vendor/github.com/oauth2-proxy/mockoidc/README.md | 218 | ||||
| -rw-r--r-- | vendor/github.com/oauth2-proxy/mockoidc/encryption.go | 188 | ||||
| -rw-r--r-- | vendor/github.com/oauth2-proxy/mockoidc/handlers.go | 527 | ||||
| -rw-r--r-- | vendor/github.com/oauth2-proxy/mockoidc/mockoidc.go | 273 | ||||
| -rw-r--r-- | vendor/github.com/oauth2-proxy/mockoidc/queue.go | 100 | ||||
| -rw-r--r-- | vendor/github.com/oauth2-proxy/mockoidc/session.go | 123 | ||||
| -rw-r--r-- | vendor/github.com/oauth2-proxy/mockoidc/user.go | 118 |
10 files changed, 1584 insertions, 0 deletions
diff --git a/vendor/github.com/oauth2-proxy/mockoidc/.gitignore b/vendor/github.com/oauth2-proxy/mockoidc/.gitignore new file mode 100644 index 0000000..66fd13c --- /dev/null +++ b/vendor/github.com/oauth2-proxy/mockoidc/.gitignore @@ -0,0 +1,15 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ diff --git a/vendor/github.com/oauth2-proxy/mockoidc/.tool-versions b/vendor/github.com/oauth2-proxy/mockoidc/.tool-versions new file mode 100644 index 0000000..9f117be --- /dev/null +++ b/vendor/github.com/oauth2-proxy/mockoidc/.tool-versions @@ -0,0 +1 @@ +golang 1.16 diff --git a/vendor/github.com/oauth2-proxy/mockoidc/LICENSE b/vendor/github.com/oauth2-proxy/mockoidc/LICENSE new file mode 100644 index 0000000..ea01ba3 --- /dev/null +++ b/vendor/github.com/oauth2-proxy/mockoidc/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 OAuth2 Proxy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/oauth2-proxy/mockoidc/README.md b/vendor/github.com/oauth2-proxy/mockoidc/README.md new file mode 100644 index 0000000..81669d4 --- /dev/null +++ b/vendor/github.com/oauth2-proxy/mockoidc/README.md @@ -0,0 +1,218 @@ +# mockoidc + +A Mock OpenID Connect Server for Authentication Unit and Integration Tests. + +Created by @NickMeves and @egrif during the [Greenhouse Software](https://medium.com/in-the-weeds) +2021 Q1 Hack Day. + +[](https://goreportcard.com/report/github.com/oauth2-proxy/mockoidc) +[](./LICENSE) +[](https://codeclimate.com/github/oauth2-proxy/mockoidc/maintainability) +[](https://codeclimate.com/github/oauth2-proxy/mockoidc/test_coverage) + +## Usage + +Import the package +``` +import "github.com/oauth2-proxy/mockoidc" +``` + +Start the MockOIDC Server. This will spin up a minimal OIDC server in its own +goroutine. It will listen on localhost on a random port. + +Then pull its configuration to integrate it with your application. Begin +testing! + +``` +m, _ := mockoidc.Run() +defer m.Shutdown() + +cfg := m.Config() +// type Config struct { +// ClientID string +// ClientSecret string +// Issuer string +// +// AccessTTL time.Duration +// RefreshTTL time.Duration +// } +``` + +### RunTLS + +Alternatively, if you provide your own `tls.Config`, the server can run with +TLS: + +``` +tlsConfig = &tls.Config{ + // ...your TLS settings +} + +m, _ := mockoidc.RunTLS(tlsConfig) +defer m.Shutdown() +``` + +### Endpoints + +The following endpoints are implemented. They can either be pulled from the +OIDC discovery document (`m.Issuer() + "/.well-known/openid-configuration`) +or retrieved directly from the MockOIDC server. + +``` +m, _ := mockoidc.Run() +defer m.Shutdown() + +m.Issuer() +m.DiscoveryEndpoint() +m.AuthorizationEndpoint() +m.TokenEndpoint() +m.UserinfoEndpoint() +m.JWKSEndpoint() +``` + +### Seeding Users and Codes + +By default, calls to the `authorization_endpoint` will start a session as if +the `mockoidc.DefaultUser()` had logged in, and it will return a random code +for the `token_endpoint`. The User in the session started by this call to the +`authorization_endpoint` will be the one in the tokens returned by the +subsequent `token_endpoint` call. + +These can be seeded with your own test Users & codes that will be returned: + +``` +m, _ := mockoidc.Run() +defer m.Shutdown() + +user := &mockoidc.User{ + // User details... +} + +// Add the User to the queue, this will be returned by the next login +m.QueueUser(user) + +// Preset the code returned by the next login +m.QueueCode("12345") + +// ...Request to m.AuthorizationEndpoint() +``` + +### Forcing Errors + +Arbitrary errors can also be queued for handlers to return instead of their +default behavior: + +``` +m, err := mockoidc.Run() +defer m.Shutdown() + +m.QueueError(&mockoidc.ServerError{ + Code: http.StatusInternalServerError, + Error: mockoidc.InternalServerError, + Description: "Some Custom Description", +}) +``` + +### Manipulating Time + +To accurately test token expiration scenarios, the MockOIDC server's view of +time is completely mutable. + +You can override the server's view of `time.Now` + +``` +mockoidc.NowFunc = func() { //...custom logic } +``` + +As tests are running, you can fast-forward time to critical test points (e.g. +Access & Refresh Token expirations). + +``` +m, _ := mockoidc.Run() + +m.FastForward(time.Duration(1) * time.Hour) +``` + +#### Synchronizing with `jwt-go` time + +Even though we can fast-forward time, the underlying tokens processed by the +[jwt-go](https://github.com/dgrijalva/jwt-go) library still have timing logic. + +We need to synchronize our timer with theirs: + +``` +m, _ := mockoidc.Run() +defer m.Shutdown() + +// Overrides jwt.TimeFunc to m.Now +reset := m.Synchronize() + +// reset is a mockoidc.ResetTime function that reverts jwt.TimeFunc to +// its original state +defer reset() +``` + +### Manual Configuration + +Everything started up with `mockoidc.Run()` can be done manually giving the +opportunity to finely tune the settings: + +``` +// Create a fresh RSA Private Key for token signing +rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048) + +// Create an unstarted MockOIDC server +m, _ := mockoidc.NewServer(rsaKey) + +// Create the net.Listener on the exact IP:Port you want +ln, _ := net.Listen("tcp", "127.0.0.1:8080") + +tlsConfig = &tls.Config{ + // ...your TLS settings +} + +// tlsConfig can be nil if you want HTTP +m.Start(ln, tlsConfig) +defer m.Shutdown() +``` + +Nearly all the MockOIDC struct is public. If you want to update any settings +to predefined values (e.g. `clientID`, `clientSecret`, `AccessTTL`, +`RefreshTTL`) you can before calling `m.Start`. + +Additional internal components of the MockOIDC server are public if you need +to tamper with them as well: + +``` +type MockOIDC struct { + // ...other stuff + + // Normally, these would be private. Expose them publicly for + // power users. + Server *http.Server + Keypair *Keypair + SessionStore *SessionStore + UserQueue *UserQueue + ErrorQueue *ErrorQueue +} +``` + +#### Adding Middleware + +When configuring the MockOIDC server manually, you have the opportunity to add +custom middleware before starting the server (e.g. request logging, test +validators, etc). + +``` +m, _ := mockoidc.NewServer(nil) + +middleware := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + // custom middleware logic here... + next.ServeHTTP(rw, req) + // custom middleware logic here... + }) +} + +m.AddMiddleware(middleware) +``` diff --git a/vendor/github.com/oauth2-proxy/mockoidc/encryption.go b/vendor/github.com/oauth2-proxy/mockoidc/encryption.go new file mode 100644 index 0000000..0e49759 --- /dev/null +++ b/vendor/github.com/oauth2-proxy/mockoidc/encryption.go @@ -0,0 +1,188 @@ +package mockoidc + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/go-jose/go-jose/v3" + "github.com/golang-jwt/jwt/v5" +) + +const ( + CodeChallengeMethodPlain = "plain" + CodeChallengeMethodS256 = "S256" +) + +const DefaultKey = `MIIEowIBAAKCAQEAtI1Jf2zmfwLzpAjVarORtjKtmCHQtgNxqWDdVNVa` + + `gCb092tLrBRv0fTfHIJG-YpmmTrRN5yKax9bI3oSYNZJufAN3gu4TIrlLoFv6npC-k3rK-s` + + `biD2m0iz9duxe7uVSEHCJlcMas86Wa-VGBlAZQpnqh2TlaHXhyVbm-gHFGU0u26Pgv5Esw2` + + `DEwRh0l7nK1ygg8dL_NNdtnaxTYhWAVPo4Vqcl2a9n-bs65maK02IgBLpaLRUtjfjSIV17Y` + + `Bzlr6ekr7GwkDTD79d3Uc2GSSGzWqKlFtXmM9cFkfGGOYcaQLoELbkxaGfLmKI53HIxXUK2` + + `8JjVCxITGl60u_Z5bQIDAQABAoIBADzUXS7RQdcI540cbMrGNRFtgY7_1ZF9F445VFiAiT0` + + `j4uR5AcW4HPRfy8uPGNp6BpcZeeOCmh_9MHeDaS23BJ_ggMuOp0kigpRoh4w4JNiv58ukKm` + + `J8YvfssHigqltSZ5OiVrheQ2DQ-Vzgofb-hYQq1xlGpQPMs4ViAe-5KO6cwXYTL3j7PXAtE` + + `34Cl6JW36dd2U4G7EeEK8inq-zCg6U0mtyudz-6YicOLXaNKmJaSUn8pWuWqUd14mpqgo54` + + `l46mMx9d_HmG45jpMUam7qVYQ9ixtRp3vCUp5k4aSgigX0dn8pv3TGpSyq_t6g93DtMlXDY` + + `9rUjgQ3w5Y8L-kAECgYEAz0sCr--a-rXHzLDdRpsI5nzYqpwB8GOJKTADrkil_F1PfQ3SAq` + + `Gtb4ioQNO054WQYHzZFryh4joTiOkmlgjM0k8eRJ4442ayJe6vm_apxWGkAiS0szooyUpH4` + + `OqVwUaDjA7yF3PBuMc1Ub65EQU9mcsEBVdlNO_hfF_1C2LupPECgYEA3vnCJYp1MYy7zUSo` + + `v70UTP_P01J5kIFYzY4VHRI4C0xZG4w_wjgsnYbGT1n9r14W_i7EhEV1R0SxmbnrbfSt31n` + + `iZfCfzl-jq7v_q0-6gm51y1sm68jdFSgwxcRKbD41jP3BUNrfQhJdpB2FbSNAHQSng0XLVF` + + `fhDGFnzn277D0CgYAZ5glD6e-2-xcnX8GFnMET6u03A57KZeUxHCqZj8INMatIuH1QjtqYY` + + `L6Euu6TLoDHTVHiIVcoaJEgPeDwRdExRWlGsW3yG1aOnq-aEMtNOdG_4s4gxldqLrmkRCrJ` + + `pwGwcf2VKIU_jMQAno-IrNrxaAfskuq2HnJRk7uN3KJsQQKBgQC0YCcGZ3NWmhpye1Bni3W` + + `YtHhS4y0kEP7dikraMZrUyPZsqpAJdZfh9t0F5C6sZtkC1qJyvh2ZgaCKUzR4xq7BN91Fyd` + + `n9ALFOg87Xrq-aQ_FWiG573wm5y8FoutnZppl7bOutlOF2eZT25krBdvqufs1kDFnn6Q9ND` + + `J8FFAGpoQKBgDMXVHVXNCJWO13_rwakBe4a9W_lbKuVX27wgCBcu3i_lGYjggm8GPkaWk14` + + `b-reOmP3tZyZxDyX2zFyjkJpu2SWd5TlAL59vP3dzx-uyj6boWCCZHxzepli5eHXOeVW-S-` + + `gwlCAF0U0n_XJ7Qhv0_SQnxSqT-D6V1-KbbeXnO7w` + +// Keypair is an RSA Keypair & JWT KeyID used for OIDC Token signing +type Keypair struct { + PrivateKey *rsa.PrivateKey + PublicKey *rsa.PublicKey + Kid string +} + +// NewKeypair makes a Keypair off the provided rsa.PrivateKey or returns +// the package default if nil was passed +func NewKeypair(key *rsa.PrivateKey) (*Keypair, error) { + if key == nil { + return DefaultKeypair() + } + + return &Keypair{ + PrivateKey: key, + PublicKey: &key.PublicKey, + }, nil +} + +// RandomKeypair creates a random rsa.PrivateKey and generates a key pair. +// This can be compute intensive, and should be avoided if called many +// times in a test suite. +func RandomKeypair(size int) (*Keypair, error) { + key, err := rsa.GenerateKey(rand.Reader, size) + if err != nil { + return nil, err + } + + return &Keypair{ + PrivateKey: key, + PublicKey: &key.PublicKey, + }, nil +} + +// Returns the default Keypair built from DefaultKey +func DefaultKeypair() (*Keypair, error) { + keyBytes, err := base64.RawURLEncoding.DecodeString(DefaultKey) + if err != nil { + return nil, err + } + key, err := x509.ParsePKCS1PrivateKey(keyBytes) + if err != nil { + return nil, err + } + + return &Keypair{ + PrivateKey: key, + PublicKey: &key.PublicKey, + }, nil +} + +// If not manually set, computes the JWT headers' `kid` +func (k *Keypair) KeyID() (string, error) { + if k.Kid != "" { + return k.Kid, nil + } + + publicKeyDERBytes, err := x509.MarshalPKIXPublicKey(k.PublicKey) + if err != nil { + return "", err + } + + hasher := crypto.SHA256.New() + if _, err := hasher.Write(publicKeyDERBytes); err != nil { + return "", err + } + publicKeyDERHash := hasher.Sum(nil) + + k.Kid = base64.RawURLEncoding.EncodeToString(publicKeyDERHash) + + return k.Kid, nil +} + +// JWKS is the JSON JWKS representation of the rsa.PublicKey +func (k *Keypair) JWKS() ([]byte, error) { + kid, err := k.KeyID() + if err != nil { + return nil, err + } + + jwk := jose.JSONWebKey{ + Use: "sig", + Algorithm: string(jose.RS256), + Key: k.PublicKey, + KeyID: kid, + } + jwks := &jose.JSONWebKeySet{ + Keys: []jose.JSONWebKey{jwk}, + } + + return json.Marshal(jwks) +} + +// SignJWT signs jwt.Claims with the Keypair and returns a token string +func (k *Keypair) SignJWT(claims jwt.Claims) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + + kid, err := k.KeyID() + if err != nil { + return "", err + } + token.Header["kid"] = kid + + return token.SignedString(k.PrivateKey) +} + +// VerifyJWT verifies the signature of a token was signed with this Keypair +func (k *Keypair) VerifyJWT(token string, nowFunc func() time.Time) (*jwt.Token, error) { + return jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { + kid, err := k.KeyID() + if err != nil { + return nil, err + } + if tk, ok := token.Header["kid"]; ok && tk == kid { + return k.PublicKey, nil + } + return nil, errors.New("token kid does not match or is not present") + }, jwt.WithTimeFunc(nowFunc)) +} + +func randomNonce(length int) (string, error) { + b := make([]byte, length) + _, err := rand.Read(b) + if err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(b), nil +} + +func GenerateCodeChallenge(method, codeVerifier string) (string, error) { + switch method { + case CodeChallengeMethodPlain: + return codeVerifier, nil + case CodeChallengeMethodS256: + shaSum := sha256.Sum256([]byte(codeVerifier)) + return base64.RawURLEncoding.EncodeToString(shaSum[:]), nil + default: + return "", fmt.Errorf("unknown challenge method: %v", method) + } +} diff --git a/vendor/github.com/oauth2-proxy/mockoidc/handlers.go b/vendor/github.com/oauth2-proxy/mockoidc/handlers.go new file mode 100644 index 0000000..d1405f1 --- /dev/null +++ b/vendor/github.com/oauth2-proxy/mockoidc/handlers.go @@ -0,0 +1,527 @@ +package mockoidc + +import ( + "crypto/subtle" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +const ( + IssuerBase = "/oidc" + AuthorizationEndpoint = "/oidc/authorize" + TokenEndpoint = "/oidc/token" + UserinfoEndpoint = "/oidc/userinfo" + JWKSEndpoint = "/oidc/.well-known/jwks.json" + DiscoveryEndpoint = "/oidc/.well-known/openid-configuration" + + InvalidRequest = "invalid_request" + InvalidClient = "invalid_client" + InvalidGrant = "invalid_grant" + UnsupportedGrantType = "unsupported_grant_type" + InvalidScope = "invalid_scope" + //UnauthorizedClient = "unauthorized_client" + InternalServerError = "internal_server_error" + + applicationJSON = "application/json" + openidScope = "openid" +) + +var ( + GrantTypesSupported = []string{ + "authorization_code", + "refresh_token", + } + ResponseTypesSupported = []string{ + "code", + } + SubjectTypesSupported = []string{ + "public", + } + IDTokenSigningAlgValuesSupported = []string{ + "RS256", + } + ScopesSupported = []string{ + "openid", + "email", + "groups", + "profile", + } + TokenEndpointAuthMethodsSupported = []string{ + "client_secret_basic", + "client_secret_post", + } + ClaimsSupported = []string{ + "sub", + "email", + "email_verified", + "preferred_username", + "phone_number", + "address", + "groups", + "iss", + "aud", + } +) + +// Authorize implements the `authorization_endpoint` in the OIDC flow. +// It is the initial request that "authenticates" a user in the OAuth2 +// flow and redirects the client to the application `redirect_uri`. +func (m *MockOIDC) Authorize(rw http.ResponseWriter, req *http.Request) { + err := req.ParseForm() + if err != nil { + internalServerError(rw, err.Error()) + return + } + + valid := assertPresence( + []string{"scope", "state", "client_id", "response_type", "redirect_uri"}, rw, req) + if !valid { + return + } + + if !validateScope(rw, req) { + return + } + validClient := assertEqual("client_id", m.ClientID, + InvalidClient, "Invalid client id", rw, req) + if !validClient { + return + } + validType := assertEqual("response_type", "code", + UnsupportedGrantType, "Invalid response type", rw, req) + if !validType { + return + } + if !validateCodeChallengeMethodSupported(rw, req.Form.Get("code_challenge_method"), m.CodeChallengeMethodsSupported) { + return + } + + session, err := m.SessionStore.NewSession( + req.Form.Get("scope"), + req.Form.Get("nonce"), + m.UserQueue.Pop(), + req.Form.Get("code_challenge"), + req.Form.Get("code_challenge_method"), + ) + if err != nil { + internalServerError(rw, err.Error()) + return + } + + redirectURI, err := url.Parse(req.Form.Get("redirect_uri")) + if err != nil { + internalServerError(rw, err.Error()) + return + } + params, _ := url.ParseQuery(redirectURI.RawQuery) + params.Set("code", session.SessionID) + params.Set("state", req.Form.Get("state")) + redirectURI.RawQuery = params.Encode() + + http.Redirect(rw, req, redirectURI.String(), http.StatusFound) +} + +type tokenResponse struct { + AccessToken string `json:"access_token,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` + IDToken string `json:"id_token,omitempty"` + TokenType string `json:"token_type"` + ExpiresIn time.Duration `json:"expires_in"` +} + +// Token implements the `token_endpoint` in OIDC and responds to requests +// from the application servers that contain the client ID & Secret along +// with the code from the `authorization_endpoint`. It returns the various +// OAuth tokens to the application server for the User authenticated by the +// during the `authorization_endpoint` request (persisted across requests via +// the `code`). +// Reference: https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/ +func (m *MockOIDC) Token(rw http.ResponseWriter, req *http.Request) { + err := req.ParseForm() + if err != nil { + internalServerError(rw, err.Error()) + return + } + + if !m.validateTokenParams(rw, req) { + return + } + + var ( + session *Session + valid bool + ) + grantType := req.Form.Get("grant_type") + switch grantType { + case "authorization_code": + if session, valid = m.validateCodeGrant(rw, req); !valid { + return + } + + if !m.validateCodeChallenge(rw, req, session) { + return + } + case "refresh_token": + if session, valid = m.validateRefreshGrant(rw, req); !valid { + return + } + default: + errorResponse(rw, InvalidRequest, + fmt.Sprintf("Invalid grant type: %s", grantType), http.StatusBadRequest) + return + } + + tr := &tokenResponse{ + RefreshToken: req.Form.Get("refresh_token"), + TokenType: "bearer", + ExpiresIn: m.AccessTTL, + } + err = m.setTokens(tr, session, grantType) + if err != nil { + internalServerError(rw, err.Error()) + return + } + + resp, err := json.Marshal(tr) + if err != nil { + internalServerError(rw, err.Error()) + return + } + noCache(rw) + jsonResponse(rw, resp) +} + +func (m *MockOIDC) validateTokenParams(rw http.ResponseWriter, req *http.Request) bool { + if !assertPresence([]string{"client_id", "client_secret", "grant_type"}, rw, req) { + return false + } + + equal := assertEqual("client_id", m.ClientID, + InvalidClient, "Invalid client id", rw, req) + if !equal { + return false + } + equal = assertEqual("client_secret", m.ClientSecret, + InvalidClient, "Invalid client secret", rw, req) + if !equal { + return false + } + + return true +} + +func (m *MockOIDC) validateCodeGrant(rw http.ResponseWriter, req *http.Request) (*Session, bool) { + if !assertPresence([]string{"code"}, rw, req) { + return nil, false + } + equal := assertEqual("grant_type", "authorization_code", + UnsupportedGrantType, "Invalid grant type", rw, req) + if !equal { + return nil, false + } + + code := req.Form.Get("code") + session, err := m.SessionStore.GetSessionByID(code) + if err != nil || session.Granted { + errorResponse(rw, InvalidGrant, fmt.Sprintf("Invalid code: %s", code), + http.StatusUnauthorized) + return nil, false + } + session.Granted = true + + return session, true +} + +func (m *MockOIDC) validateCodeChallenge(rw http.ResponseWriter, req *http.Request, session *Session) bool { + if session.CodeChallenge == "" || session.CodeChallengeMethod == "" { + return true + } + + codeVerifier := req.Form.Get("code_verifier") + if codeVerifier == "" { + errorResponse(rw, InvalidGrant, "Invalid code verifier. Expected code but client sent none.", http.StatusUnauthorized) + return false + } + + challenge, err := GenerateCodeChallenge(session.CodeChallengeMethod, codeVerifier) + if err != nil { + errorResponse(rw, InvalidRequest, fmt.Sprintf("Invalid code verifier. %v", err.Error()), http.StatusUnauthorized) + return false + } + + if challenge != session.CodeChallenge { + errorResponse(rw, InvalidGrant, "Invalid code verifier. Code challenge did not match hashed code verifier.", http.StatusUnauthorized) + return false + } + + return true +} + +func (m *MockOIDC) validateRefreshGrant(rw http.ResponseWriter, req *http.Request) (*Session, bool) { + if !assertPresence([]string{"refresh_token"}, rw, req) { + return nil, false + } + + equal := assertEqual("grant_type", "refresh_token", + UnsupportedGrantType, "Invalid grant type", rw, req) + if !equal { + return nil, false + } + + refreshToken := req.Form.Get("refresh_token") + token, authorized := m.authorizeToken(refreshToken, rw) + if !authorized { + return nil, false + } + + session, err := m.SessionStore.GetSessionByToken(token) + if err != nil { + errorResponse(rw, InvalidGrant, "Invalid refresh token", + http.StatusUnauthorized) + return nil, false + } + return session, true +} + +func (m *MockOIDC) setTokens(tr *tokenResponse, s *Session, grantType string) error { + var err error + tr.AccessToken, err = s.AccessToken(m.Config(), m.Keypair, m.Now()) + if err != nil { + return err + } + if len(s.Scopes) > 0 && s.Scopes[0] == openidScope { + tr.IDToken, err = s.IDToken(m.Config(), m.Keypair, m.Now()) + if err != nil { + return err + } + } + if grantType != "refresh_token" { + tr.RefreshToken, err = s.RefreshToken(m.Config(), m.Keypair, m.Now()) + if err != nil { + return err + } + } + return nil +} + +// Userinfo returns the User details for the User associated with the passed +// Access Token. Data is scoped down to the session's access scope set in the +// initial `authorization_endpoint` call. +func (m *MockOIDC) Userinfo(rw http.ResponseWriter, req *http.Request) { + token, authorized := m.authorizeBearer(rw, req) + if !authorized { + return + } + + session, err := m.SessionStore.GetSessionByToken(token) + if err != nil { + internalServerError(rw, err.Error()) + return + } + + resp, err := session.User.Userinfo(session.Scopes) + if err != nil { + internalServerError(rw, err.Error()) + return + } + jsonResponse(rw, resp) +} + +type discoveryResponse struct { + Issuer string `json:"issuer"` + AuthorizationEndpoint string `json:"authorization_endpoint"` + TokenEndpoint string `json:"token_endpoint"` + JWKSUri string `json:"jwks_uri"` + UserinfoEndpoint string `json:"userinfo_endpoint"` + + GrantTypesSupported []string `json:"grant_types_supported"` + ResponseTypesSupported []string `json:"response_types_supported"` + SubjectTypesSupported []string `json:"subject_types_supported"` + IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"` + ScopesSupported []string `json:"scopes_supported"` + TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"` + ClaimsSupported []string `json:"claims_supported"` + CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"` +} + +// Discovery renders the OIDC discovery document and partial RFC-8414 authorization +// server metadata hosted at `/.well-known/openid-configuration`. +func (m *MockOIDC) Discovery(rw http.ResponseWriter, _ *http.Request) { + discovery := &discoveryResponse{ + Issuer: m.Issuer(), + AuthorizationEndpoint: m.AuthorizationEndpoint(), + TokenEndpoint: m.TokenEndpoint(), + JWKSUri: m.JWKSEndpoint(), + UserinfoEndpoint: m.UserinfoEndpoint(), + + GrantTypesSupported: GrantTypesSupported, + ResponseTypesSupported: ResponseTypesSupported, + SubjectTypesSupported: SubjectTypesSupported, + IDTokenSigningAlgValuesSupported: IDTokenSigningAlgValuesSupported, + ScopesSupported: ScopesSupported, + TokenEndpointAuthMethodsSupported: TokenEndpointAuthMethodsSupported, + ClaimsSupported: ClaimsSupported, + CodeChallengeMethodsSupported: m.CodeChallengeMethodsSupported, + } + + resp, err := json.Marshal(discovery) + if err != nil { + internalServerError(rw, err.Error()) + return + } + jsonResponse(rw, resp) +} + +// JWKS returns the public key in JWKS format to verify in tokens +// signed with our Keypair.PrivateKey. +func (m *MockOIDC) JWKS(rw http.ResponseWriter, _ *http.Request) { + jwks, err := m.Keypair.JWKS() + if err != nil { + internalServerError(rw, err.Error()) + return + } + + jsonResponse(rw, jwks) +} + +func (m *MockOIDC) authorizeBearer(rw http.ResponseWriter, req *http.Request) (*jwt.Token, bool) { + header := req.Header.Get("Authorization") + parts := strings.SplitN(header, " ", 2) + if len(parts) < 2 || parts[0] != "Bearer" { + errorResponse(rw, InvalidRequest, "Invalid authorization header", + http.StatusUnauthorized) + return nil, false + } + + return m.authorizeToken(parts[1], rw) +} + +func (m *MockOIDC) authorizeToken(t string, rw http.ResponseWriter) (*jwt.Token, bool) { + token, err := m.Keypair.VerifyJWT(t, m.Now) + if err != nil { + errorResponse(rw, InvalidRequest, fmt.Sprintf("Invalid token: %v", err), http.StatusUnauthorized) + return nil, false + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + internalServerError(rw, "Unable to extract token claims") + return nil, false + } + exp, ok := claims["exp"].(float64) + if !ok { + internalServerError(rw, "Unable to extract token expiration") + return nil, false + } + if m.Now().Unix() > int64(exp) { + errorResponse(rw, InvalidRequest, "The token is expired", http.StatusUnauthorized) + return nil, false + } + return token, true +} + +func assertPresence(params []string, rw http.ResponseWriter, req *http.Request) bool { + for _, param := range params { + if req.Form.Get(param) != "" { + continue + } + errorResponse( + rw, + InvalidRequest, + fmt.Sprintf("The request is missing the required parameter: %s", param), + http.StatusBadRequest, + ) + return false + } + return true +} + +func assertEqual(param, value, errorType, errorMsg string, rw http.ResponseWriter, req *http.Request) bool { + formValue := req.Form.Get(param) + if subtle.ConstantTimeCompare([]byte(value), []byte(formValue)) == 0 { + errorResponse(rw, errorType, fmt.Sprintf("%s: %s", errorMsg, formValue), + http.StatusUnauthorized) + return false + } + return true +} + +func validateScope(rw http.ResponseWriter, req *http.Request) bool { + allowed := make(map[string]struct{}) + for _, scope := range ScopesSupported { + allowed[scope] = struct{}{} + } + + scopes := strings.Split(req.Form.Get("scope"), " ") + for _, scope := range scopes { + if _, ok := allowed[scope]; !ok { + errorResponse(rw, InvalidScope, fmt.Sprintf("Unsupported scope: %s", scope), + http.StatusBadRequest) + return false + } + } + return true +} + +func validateCodeChallengeMethodSupported(rw http.ResponseWriter, method string, supportedMethods []string) bool { + if method != "" && !contains(method, supportedMethods) { + errorResponse(rw, InvalidRequest, "Invalid code challenge method", http.StatusBadRequest) + return false + } + return true +} + +func errorResponse(rw http.ResponseWriter, error, description string, statusCode int) { + errJSON := map[string]string{ + "error": error, + "error_description": description, + } + resp, err := json.Marshal(errJSON) + if err != nil { + http.Error(rw, error, http.StatusInternalServerError) + } + + noCache(rw) + rw.Header().Set("Content-Type", applicationJSON) + rw.WriteHeader(statusCode) + + _, err = rw.Write(resp) + if err != nil { + panic(err) + } +} + +func internalServerError(rw http.ResponseWriter, errorMsg string) { + errorResponse(rw, InternalServerError, errorMsg, http.StatusInternalServerError) +} + +func jsonResponse(rw http.ResponseWriter, data []byte) { + noCache(rw) + rw.Header().Set("Content-Type", applicationJSON) + rw.WriteHeader(http.StatusOK) + + _, err := rw.Write(data) + if err != nil { + panic(err) + } +} + +func noCache(rw http.ResponseWriter) { + rw.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0") + rw.Header().Set("Pragma", "no-cache") +} + +func contains(value string, list []string) bool { + for _, element := range list { + if element == value { + return true + } + } + return false +} diff --git a/vendor/github.com/oauth2-proxy/mockoidc/mockoidc.go b/vendor/github.com/oauth2-proxy/mockoidc/mockoidc.go new file mode 100644 index 0000000..e66ca58 --- /dev/null +++ b/vendor/github.com/oauth2-proxy/mockoidc/mockoidc.go @@ -0,0 +1,273 @@ +package mockoidc + +import ( + "context" + "crypto/rsa" + "crypto/tls" + "errors" + "fmt" + "net" + "net/http" + "time" +) + +// NowFunc is an overrideable version of `time.Now`. Tests that need to +// manipulate time can use their own `func() Time` function. +var NowFunc = time.Now + +// MockOIDC is a minimal OIDC server for use in OIDC authentication +// integration testing. +type MockOIDC struct { + ClientID string + ClientSecret string + + AccessTTL time.Duration + RefreshTTL time.Duration + + CodeChallengeMethodsSupported []string + + // Normally, these would be private. Expose them publicly for + // power users. + Server *http.Server + Keypair *Keypair + SessionStore *SessionStore + UserQueue *UserQueue + ErrorQueue *ErrorQueue + + tlsConfig *tls.Config + middleware []func(http.Handler) http.Handler + fastForward time.Duration +} + +// Config gives the various settings MockOIDC starts with that a test +// application server would need to be configured with. +type Config struct { + ClientID string + ClientSecret string + Issuer string + + AccessTTL time.Duration + RefreshTTL time.Duration + + CodeChallengeMethodsSupported []string +} + +// NewServer configures a new MockOIDC that isn't started. An existing +// rsa.PrivateKey can be passed for token signing operations in case +// the default Keypair isn't desired. +func NewServer(key *rsa.PrivateKey) (*MockOIDC, error) { + clientID, err := randomNonce(24) + if err != nil { + return nil, err + } + clientSecret, err := randomNonce(24) + if err != nil { + return nil, err + } + keypair, err := NewKeypair(key) + if err != nil { + return nil, err + } + + return &MockOIDC{ + ClientID: clientID, + ClientSecret: clientSecret, + AccessTTL: time.Duration(10) * time.Minute, + RefreshTTL: time.Duration(60) * time.Minute, + CodeChallengeMethodsSupported: []string{"plain", "S256"}, + Keypair: keypair, + SessionStore: NewSessionStore(), + UserQueue: &UserQueue{}, + ErrorQueue: &ErrorQueue{}, + }, nil +} + +// Run creates a default MockOIDC server and starts it +func Run() (*MockOIDC, error) { + return RunTLS(nil) +} + +// RunTLS creates a default MockOIDC server and starts it. It takes a +// tester configured tls.Config for TLS support. +func RunTLS(cfg *tls.Config) (*MockOIDC, error) { + m, err := NewServer(nil) + if err != nil { + return nil, err + } + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, err + } + return m, m.Start(ln, cfg) +} + +// Start starts the MockOIDC server in its own Goroutine on the provided +// net.Listener. In generic `Run`, this defaults to `127.0.0.1:0` +func (m *MockOIDC) Start(ln net.Listener, cfg *tls.Config) error { + if m.Server != nil { + return errors.New("server already started") + } + + handler := http.NewServeMux() + handler.Handle(AuthorizationEndpoint, m.chainMiddleware(m.Authorize)) + handler.Handle(TokenEndpoint, m.chainMiddleware(m.Token)) + handler.Handle(UserinfoEndpoint, m.chainMiddleware(m.Userinfo)) + handler.Handle(JWKSEndpoint, m.chainMiddleware(m.JWKS)) + handler.Handle(DiscoveryEndpoint, m.chainMiddleware(m.Discovery)) + + m.Server = &http.Server{ + Addr: ln.Addr().String(), + Handler: handler, + TLSConfig: cfg, + } + // Track this to know if we are https + m.tlsConfig = cfg + + go func() { + err := m.Server.Serve(ln) + if err != nil && err != http.ErrServerClosed { + panic(err) + } + }() + + return nil +} + +// Shutdown stops the MockOIDC server. Use this to cleanup test runs. +func (m *MockOIDC) Shutdown() error { + return m.Server.Shutdown(context.Background()) +} + +func (m *MockOIDC) AddMiddleware(mw func(http.Handler) http.Handler) error { + if m.Server != nil { + return errors.New("server already started") + } + + m.middleware = append(m.middleware, mw) + return nil +} + +// Config returns the Config with options a connection application or unit +// tests need to be aware of. +func (m *MockOIDC) Config() *Config { + return &Config{ + ClientID: m.ClientID, + ClientSecret: m.ClientSecret, + Issuer: m.Issuer(), + CodeChallengeMethodsSupported: m.CodeChallengeMethodsSupported, + AccessTTL: m.AccessTTL, + RefreshTTL: m.RefreshTTL, + } +} + +// QueueUser allows adding mock User objects to the authentication queue. +// Calls to the `authorization_endpoint` will pop these mock User objects +// off the queue and create a session with them. +func (m *MockOIDC) QueueUser(user User) { + m.UserQueue.Push(user) +} + +// QueueCode allows adding mock code strings to the authentication queue. +// Calls to the `authorization_endpoint` will pop these code strings +// off the queue and create a session with them and return them as the +// code parameter in the response. +func (m *MockOIDC) QueueCode(code string) { + m.SessionStore.CodeQueue.Push(code) +} + +// QueueError allows queueing arbitrary errors for the next handler calls +// to return. +func (m *MockOIDC) QueueError(se *ServerError) { + m.ErrorQueue.Push(se) +} + +// FastForward moves the MockOIDC's internal view of time forward. +// Use this to test token expirations in your tests. +func (m *MockOIDC) FastForward(d time.Duration) time.Duration { + m.fastForward = m.fastForward + d + return m.fastForward +} + +// Now is what MockOIDC thinks time.Now is +func (m *MockOIDC) Now() time.Time { + return NowFunc().Add(m.fastForward) +} + +// Addr returns the server address (if started) +func (m *MockOIDC) Addr() string { + if m.Server == nil { + return "" + } + proto := "http" + if m.tlsConfig != nil { + proto = "https" + } + return fmt.Sprintf("%s://%s", proto, m.Server.Addr) +} + +// Issuer returns the OIDC Issuer that will be in `iss` token claims +func (m *MockOIDC) Issuer() string { + if m.Server == nil { + return "" + } + return m.Addr() + IssuerBase +} + +// DiscoveryEndpoint returns the full `/.well-known/openid-configuration` URL +func (m *MockOIDC) DiscoveryEndpoint() string { + if m.Server == nil { + return "" + } + return m.Addr() + DiscoveryEndpoint +} + +// AuthorizationEndpoint returns the OIDC `authorization_endpoint` +func (m *MockOIDC) AuthorizationEndpoint() string { + if m.Server == nil { + return "" + } + return m.Addr() + AuthorizationEndpoint +} + +// TokenEndpoint returns the OIDC `token_endpoint` +func (m *MockOIDC) TokenEndpoint() string { + if m.Server == nil { + return "" + } + return m.Addr() + TokenEndpoint +} + +// UserinfoEndpoint returns the OIDC `userinfo_endpoint` +func (m *MockOIDC) UserinfoEndpoint() string { + if m.Server == nil { + return "" + } + return m.Addr() + UserinfoEndpoint +} + +// JWKSEndpoint returns the OIDC `jwks_uri` +func (m *MockOIDC) JWKSEndpoint() string { + if m.Server == nil { + return "" + } + return m.Addr() + JWKSEndpoint +} + +func (m *MockOIDC) chainMiddleware(endpoint func(http.ResponseWriter, *http.Request)) http.Handler { + chain := m.forceError(http.HandlerFunc(endpoint)) + for i := len(m.middleware) - 1; i >= 0; i-- { + mw := m.middleware[i] + chain = mw(chain) + } + return chain +} + +func (m *MockOIDC) forceError(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if se := m.ErrorQueue.Pop(); se != nil { + errorResponse(rw, se.Error, se.Description, se.Code) + } else { + next.ServeHTTP(rw, req) + } + }) +} diff --git a/vendor/github.com/oauth2-proxy/mockoidc/queue.go b/vendor/github.com/oauth2-proxy/mockoidc/queue.go new file mode 100644 index 0000000..bd706d3 --- /dev/null +++ b/vendor/github.com/oauth2-proxy/mockoidc/queue.go @@ -0,0 +1,100 @@ +package mockoidc + +import "sync" + +// UserQueue manages the queue of Users returned for each +// call to the authorize endpoint +type UserQueue struct { + sync.Mutex + Queue []User +} + +// CodeQueue manages the queue of codes returned for each +// call to the authorize endpoint +type CodeQueue struct { + sync.Mutex + Queue []string +} + +// ErrorQueue manages the queue of errors for handlers to return +type ErrorQueue struct { + sync.Mutex + Queue []*ServerError +} + +// ServerError is a tester-defined error for a handler to return +type ServerError struct { + Code int + Error string + Description string +} + +// Push adds a User to the Queue to be set in subsequent calls to the +// `authorization_endpoint` +func (q *UserQueue) Push(user User) { + q.Lock() + defer q.Unlock() + q.Queue = append(q.Queue, user) +} + +// Pop a User from the Queue. If empty, return `DefaultUser()` +func (q *UserQueue) Pop() User { + q.Lock() + defer q.Unlock() + + if len(q.Queue) == 0 { + return DefaultUser() + } + + var user User + user, q.Queue = q.Queue[0], q.Queue[1:] + return user +} + +// Push adds a code to the Queue to be returned by subsequent +// `authorization_endpoint` calls as the code +func (q *CodeQueue) Push(code string) { + q.Lock() + defer q.Unlock() + q.Queue = append(q.Queue, code) +} + +// Pop a `code` from the Queue. If empty, return a random code +func (q *CodeQueue) Pop() (string, error) { + q.Lock() + defer q.Unlock() + + if len(q.Queue) == 0 { + code, err := randomNonce(24) + if err != nil { + return "", err + } + return code, nil + } + + var code string + code, q.Queue = q.Queue[0], q.Queue[1:] + return code, nil +} + +// Push adds a ServerError to the Queue to be returned in subsequent +// handler calls +func (q *ErrorQueue) Push(se *ServerError) { + q.Lock() + defer q.Unlock() + q.Queue = append(q.Queue, se) +} + +// Pop a ServerError from the Queue. If empty, return nil +func (q *ErrorQueue) Pop() *ServerError { + q.Lock() + defer q.Unlock() + + if len(q.Queue) == 0 { + return nil + } + + var se *ServerError + se, q.Queue = q.Queue[0], q.Queue[1:] + return se +} diff --git a/vendor/github.com/oauth2-proxy/mockoidc/session.go b/vendor/github.com/oauth2-proxy/mockoidc/session.go new file mode 100644 index 0000000..4107a38 --- /dev/null +++ b/vendor/github.com/oauth2-proxy/mockoidc/session.go @@ -0,0 +1,123 @@ +package mockoidc + +import ( + "errors" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +// Session stores a User and their OIDC options across requests +type Session struct { + SessionID string + Scopes []string + OIDCNonce string + User User + Granted bool + CodeChallenge string + CodeChallengeMethod string +} + +// SessionStore manages our Session objects +type SessionStore struct { + Store map[string]*Session + CodeQueue *CodeQueue +} + +// IDTokenClaims are the mandatory claims any User.Claims implementation +// should use in their jwt.Claims building. +type IDTokenClaims struct { + Nonce string `json:"nonce,omitempty"` + *jwt.RegisteredClaims +} + +// NewSessionStore initializes the SessionStore for this server +func NewSessionStore() *SessionStore { + return &SessionStore{ + Store: make(map[string]*Session), + CodeQueue: &CodeQueue{}, + } +} + +// NewSession creates a new Session for a User +func (ss *SessionStore) NewSession(scope string, nonce string, user User, codeChallenge string, codeChallengeMethod string) (*Session, error) { + sessionID, err := ss.CodeQueue.Pop() + if err != nil { + return nil, err + } + + session := &Session{ + SessionID: sessionID, + Scopes: strings.Split(scope, " "), + OIDCNonce: nonce, + User: user, + CodeChallenge: codeChallenge, + CodeChallengeMethod: codeChallengeMethod, + } + ss.Store[sessionID] = session + + return session, nil +} + +// GetSessionByID looks up the Session +func (ss *SessionStore) GetSessionByID(id string) (*Session, error) { + session, ok := ss.Store[id] + if !ok { + return nil, errors.New("session not found") + } + return session, nil +} + +// GetSessionByToken decodes a token and looks up a Session based on the +// session ID claim. +func (ss *SessionStore) GetSessionByToken(token *jwt.Token) (*Session, error) { + claims, ok := token.Claims.(jwt.MapClaims) + if !ok || !token.Valid { + return nil, errors.New("invalid token") + } + + sessionID := claims["jti"].(string) + return ss.GetSessionByID(sessionID) +} + +// AccessToken returns the JWT token with the appropriate claims for +// an access token +func (s *Session) AccessToken(config *Config, kp *Keypair, now time.Time) (string, error) { + claims := s.registeredClaims(config, config.AccessTTL, now) + return kp.SignJWT(claims) +} + +// RefreshToken returns the JWT token with the appropriate claims for +// a refresh token +func (s *Session) RefreshToken(config *Config, kp *Keypair, now time.Time) (string, error) { + claims := s.registeredClaims(config, config.RefreshTTL, now) + return kp.SignJWT(claims) +} + +// IDToken returns the JWT token with the appropriate claims for a user +// based on the scopes set. +func (s *Session) IDToken(config *Config, kp *Keypair, now time.Time) (string, error) { + base := &IDTokenClaims{ + RegisteredClaims: s.registeredClaims(config, config.AccessTTL, now), + Nonce: s.OIDCNonce, + } + claims, err := s.User.Claims(s.Scopes, base) + if err != nil { + return "", err + } + + return kp.SignJWT(claims) +} + +func (s *Session) registeredClaims(config *Config, ttl time.Duration, now time.Time) *jwt.RegisteredClaims { + return &jwt.RegisteredClaims{ + Audience: jwt.ClaimStrings{config.ClientID}, + ExpiresAt: jwt.NewNumericDate(now.Add(ttl)), + ID: s.SessionID, + IssuedAt: jwt.NewNumericDate(now), + Issuer: config.Issuer, + NotBefore: jwt.NewNumericDate(now), + Subject: s.User.ID(), + } +} diff --git a/vendor/github.com/oauth2-proxy/mockoidc/user.go b/vendor/github.com/oauth2-proxy/mockoidc/user.go new file mode 100644 index 0000000..cc6615f --- /dev/null +++ b/vendor/github.com/oauth2-proxy/mockoidc/user.go @@ -0,0 +1,118 @@ +package mockoidc + +import ( + "encoding/json" + + "github.com/golang-jwt/jwt/v5" +) + +// User represents a mock user that the server will grant Oauth tokens for. +// Calls to the `authorization_endpoint` will pop any mock Users added to the +// `UserQueue`. Otherwise `DefaultUser()` is returned. +type User interface { + // Unique ID for the User. This will be the Subject claim + ID() string + + // Userinfo returns the Userinfo JSON representation of a User with data + // appropriate for the passed scope []string. + Userinfo([]string) ([]byte, error) + + // Claims returns the ID Token Claims for a User with data appropriate for + // the passed scope []string. It builds off the passed BaseIDTokenClaims. + Claims([]string, *IDTokenClaims) (jwt.Claims, error) +} + +// MockUser is a default implementation of the User interface +type MockUser struct { + Subject string + Email string + EmailVerified bool + PreferredUsername string + Phone string + Address string + Groups []string +} + +// DefaultUser returns a default MockUser that is set in +// `authorization_endpoint` if the UserQueue is empty. +func DefaultUser() *MockUser { + return &MockUser{ + Subject: "1234567890", + Email: "jane.doe@example.com", + PreferredUsername: "jane.doe", + Phone: "555-987-6543", + Address: "123 Main Street", + Groups: []string{"engineering", "design"}, + EmailVerified: true, + } +} + +type mockUserinfo struct { + Email string `json:"email,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` + Phone string `json:"phone_number,omitempty"` + Address string `json:"address,omitempty"` + Groups []string `json:"groups,omitempty"` +} + +func (u *MockUser) ID() string { + return u.Subject +} + +func (u *MockUser) Userinfo(scope []string) ([]byte, error) { + user := u.scopedClone(scope) + + info := &mockUserinfo{ + Email: user.Email, + PreferredUsername: user.PreferredUsername, + Phone: user.Phone, + Address: user.Address, + Groups: user.Groups, + } + + return json.Marshal(info) +} + +type mockClaims struct { + *IDTokenClaims + Email string `json:"email,omitempty"` + EmailVerified bool `json:"email_verified,omitempty"` + PreferredUsername string `json:"preferred_username,omitempty"` + Phone string `json:"phone_number,omitempty"` + Address string `json:"address,omitempty"` + Groups []string `json:"groups,omitempty"` +} + +func (u *MockUser) Claims(scope []string, claims *IDTokenClaims) (jwt.Claims, error) { + user := u.scopedClone(scope) + + return &mockClaims{ + IDTokenClaims: claims, + Email: user.Email, + EmailVerified: user.EmailVerified, + PreferredUsername: user.PreferredUsername, + Phone: user.Phone, + Address: user.Address, + Groups: user.Groups, + }, nil +} + +func (u *MockUser) scopedClone(scopes []string) *MockUser { + clone := &MockUser{ + Subject: u.Subject, + } + for _, scope := range scopes { + switch scope { + case "profile": + clone.PreferredUsername = u.PreferredUsername + clone.Address = u.Address + clone.Phone = u.Phone + case "email": + clone.Email = u.Email + clone.EmailVerified = u.EmailVerified + case "groups": + clone.Groups = append(make([]string, 0, len(u.Groups)), u.Groups...) + } + } + return clone +} |
