summaryrefslogtreecommitdiff
path: root/vendor/github.com/oauth2-proxy/mockoidc
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-05-11 21:12:57 -0600
committermo khan <mo@mokhan.ca>2025-05-11 21:12:57 -0600
commit60440f90dca28e99a31dd328c5f6d5dc0f9b6a2e (patch)
tree2f54adf55086516f162f0a55a5347e6b25f7f176 /vendor/github.com/oauth2-proxy/mockoidc
parent05ca9b8d3a9c7203a3a3b590beaa400900bd9007 (diff)
chore: vendor go dependencies
Diffstat (limited to 'vendor/github.com/oauth2-proxy/mockoidc')
-rw-r--r--vendor/github.com/oauth2-proxy/mockoidc/.gitignore15
-rw-r--r--vendor/github.com/oauth2-proxy/mockoidc/.tool-versions1
-rw-r--r--vendor/github.com/oauth2-proxy/mockoidc/LICENSE21
-rw-r--r--vendor/github.com/oauth2-proxy/mockoidc/README.md218
-rw-r--r--vendor/github.com/oauth2-proxy/mockoidc/encryption.go188
-rw-r--r--vendor/github.com/oauth2-proxy/mockoidc/handlers.go527
-rw-r--r--vendor/github.com/oauth2-proxy/mockoidc/mockoidc.go273
-rw-r--r--vendor/github.com/oauth2-proxy/mockoidc/queue.go100
-rw-r--r--vendor/github.com/oauth2-proxy/mockoidc/session.go123
-rw-r--r--vendor/github.com/oauth2-proxy/mockoidc/user.go118
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.
+
+[![Go Report Card](https://goreportcard.com/badge/github.com/oauth2-proxy/mockoidc)](https://goreportcard.com/report/github.com/oauth2-proxy/mockoidc)
+[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
+[![Maintainability](https://api.codeclimate.com/v1/badges/99c0561090d1002dc7e3/maintainability)](https://codeclimate.com/github/oauth2-proxy/mockoidc/maintainability)
+[![Test Coverage](https://api.codeclimate.com/v1/badges/99c0561090d1002dc7e3/test_coverage)](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
+}