mockoidc

package module
v0.0.0-...-e4316ad Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 23, 2022 License: MIT Imports: 20 Imported by: 0

README

mockoidc

A Mock OpenID Connect Server for Authentication Unit and Integration Tests.

Created by @NickMeves and @egrif during the Greenhouse Software 2021 Q1 Hack Day.

Go Report Card MIT licensed Maintainability 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 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)

Documentation

Index

Constants

View Source
const (
	CodeChallengeMethodPlain = "plain"
	CodeChallengeMethodS256  = "S256"
)
View Source
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"
)
View Source
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`

Variables

View Source
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",
	}
)
View Source
var NowFunc = time.Now

NowFunc is an overrideable version of `time.Now`. Tests that need to manipulate time can use their own `func() Time` function.

Functions

func GenerateCodeChallenge

func GenerateCodeChallenge(method, codeVerifier string) (string, error)

Types

type CodeQueue

type CodeQueue struct {
	sync.Mutex
	Queue []string
}

CodeQueue manages the queue of codes returned for each call to the authorize endpoint

func (*CodeQueue) Pop

func (q *CodeQueue) Pop() (string, error)

Pop a `code` from the Queue. If empty, return a random code

func (*CodeQueue) Push

func (q *CodeQueue) Push(code string)

Push adds a code to the Queue to be returned by subsequent `authorization_endpoint` calls as the code

type Config

type Config struct {
	ClientID     string
	ClientSecret string
	Issuer       string

	AccessTTL  time.Duration
	RefreshTTL time.Duration

	CodeChallengeMethodsSupported []string
}

Config gives the various settings MockOIDC starts with that a test application server would need to be configured with.

type ErrorQueue

type ErrorQueue struct {
	sync.Mutex
	Queue []*ServerError
}

ErrorQueue manages the queue of errors for handlers to return

func (*ErrorQueue) Pop

func (q *ErrorQueue) Pop() *ServerError

Pop a ServerError from the Queue. If empty, return nil

func (*ErrorQueue) Push

func (q *ErrorQueue) Push(se *ServerError)

Push adds a ServerError to the Queue to be returned in subsequent handler calls

type IDTokenClaims

type IDTokenClaims struct {
	Nonce string `json:"nonce,omitempty"`
	*jwt.StandardClaims
}

IDTokenClaims are the mandatory claims any User.Claims implementation should use in their jwt.Claims building.

type Keypair

type Keypair struct {
	PrivateKey *rsa.PrivateKey
	PublicKey  *rsa.PublicKey
	Kid        string
}

Keypair is an RSA Keypair & JWT KeyID used for OIDC Token signing

func DefaultKeypair

func DefaultKeypair() (*Keypair, error)

Returns the default Keypair built from DefaultKey

func NewKeypair

func NewKeypair(key *rsa.PrivateKey) (*Keypair, error)

NewKeypair makes a Keypair off the provided rsa.PrivateKey or returns the package default if nil was passed

func RandomKeypair

func RandomKeypair(size int) (*Keypair, error)

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 (*Keypair) JWKS

func (k *Keypair) JWKS() ([]byte, error)

JWKS is the JSON JWKS representation of the rsa.PublicKey

func (*Keypair) KeyID

func (k *Keypair) KeyID() (string, error)

If not manually set, computes the JWT headers' `kid`

func (*Keypair) SignJWT

func (k *Keypair) SignJWT(claims jwt.Claims) (string, error)

SignJWT signs jwt.Claims with the Keypair and returns a token string

func (*Keypair) VerifyJWT

func (k *Keypair) VerifyJWT(token string) (*jwt.Token, error)

VerifyJWT verifies the signature of a token was signed with this Keypair

type MockOIDC

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
	// contains filtered or unexported fields
}

MockOIDC is a minimal OIDC server for use in OIDC authentication integration testing.

func NewServer

func NewServer(key *rsa.PrivateKey) (*MockOIDC, error)

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 Run

func Run() (*MockOIDC, error)

Run creates a default MockOIDC server and starts it

func RunTLS

func RunTLS(cfg *tls.Config) (*MockOIDC, error)

RunTLS creates a default MockOIDC server and starts it. It takes a tester configured tls.Config for TLS support.

func (*MockOIDC) AddMiddleware

func (m *MockOIDC) AddMiddleware(mw func(http.Handler) http.Handler) error

func (*MockOIDC) Addr

func (m *MockOIDC) Addr() string

Addr returns the server address (if started)

func (*MockOIDC) AuthorizationEndpoint

func (m *MockOIDC) AuthorizationEndpoint() string

AuthorizationEndpoint returns the OIDC `authorization_endpoint`

func (*MockOIDC) Authorize

func (m *MockOIDC) Authorize(rw http.ResponseWriter, req *http.Request)

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 (*MockOIDC) ChainMiddleware

func (m *MockOIDC) ChainMiddleware(endpoint func(http.ResponseWriter, *http.Request)) http.Handler

func (*MockOIDC) Config

func (m *MockOIDC) Config() *Config

Config returns the Config with options a connection application or unit tests need to be aware of.

func (*MockOIDC) Discovery

func (m *MockOIDC) Discovery(rw http.ResponseWriter, _ *http.Request)

Discovery renders the OIDC discovery document and partial RFC-8414 authorization server metadata hosted at `/.well-known/openid-configuration`.

func (*MockOIDC) DiscoveryEndpoint

func (m *MockOIDC) DiscoveryEndpoint() string

DiscoveryEndpoint returns the full `/.well-known/openid-configuration` URL

func (*MockOIDC) FastForward

func (m *MockOIDC) FastForward(d time.Duration) time.Duration

FastForward moves the MockOIDC's internal view of time forward. Use this to test token expirations in your tests.

func (*MockOIDC) Issuer

func (m *MockOIDC) Issuer() string

Issuer returns the OIDC Issuer that will be in `iss` token claims

func (*MockOIDC) JWKS

func (m *MockOIDC) JWKS(rw http.ResponseWriter, _ *http.Request)

JWKS returns the public key in JWKS format to verify in tokens signed with our Keypair.PrivateKey.

func (*MockOIDC) JWKSEndpoint

func (m *MockOIDC) JWKSEndpoint() string

JWKSEndpoint returns the OIDC `jwks_uri`

func (*MockOIDC) Now

func (m *MockOIDC) Now() time.Time

Now is what MockOIDC thinks time.Now is

func (*MockOIDC) QueueCode

func (m *MockOIDC) QueueCode(code string)

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 (*MockOIDC) QueueError

func (m *MockOIDC) QueueError(se *ServerError)

QueueError allows queueing arbitrary errors for the next handler calls to return.

func (*MockOIDC) QueueUser

func (m *MockOIDC) QueueUser(user User)

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 (*MockOIDC) Shutdown

func (m *MockOIDC) Shutdown() error

Shutdown stops the MockOIDC server. Use this to cleanup test runs.

func (*MockOIDC) Start

func (m *MockOIDC) Start(ln net.Listener, cfg *tls.Config) error

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 (*MockOIDC) Synchronize

func (m *MockOIDC) Synchronize() TimeReset

Synchronize sets the jwt.TimeFunc to our mutated view of time. It returns a func that can reset it to its original state.

func (*MockOIDC) Token

func (m *MockOIDC) Token(rw http.ResponseWriter, req *http.Request)

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 (*MockOIDC) TokenEndpoint

func (m *MockOIDC) TokenEndpoint() string

TokenEndpoint returns the OIDC `token_endpoint`

func (*MockOIDC) Userinfo

func (m *MockOIDC) Userinfo(rw http.ResponseWriter, req *http.Request)

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 (*MockOIDC) UserinfoEndpoint

func (m *MockOIDC) UserinfoEndpoint() string

UserinfoEndpoint returns the OIDC `userinfo_endpoint`

type MockUser

type MockUser struct {
	Subject           string
	Email             string
	EmailVerified     bool
	PreferredUsername string
	Phone             string
	Address           string
	Groups            []string
}

MockUser is a default implementation of the User interface

func DefaultUser

func DefaultUser() *MockUser

DefaultUser returns a default MockUser that is set in `authorization_endpoint` if the UserQueue is empty.

func (*MockUser) Claims

func (u *MockUser) Claims(scope []string, claims *IDTokenClaims) (jwt.Claims, error)

func (*MockUser) ID

func (u *MockUser) ID() string

func (*MockUser) Userinfo

func (u *MockUser) Userinfo(scope []string) ([]byte, error)

type ServerError

type ServerError struct {
	Code        int
	Error       string
	Description string
}

ServerError is a tester-defined error for a handler to return

type Session

type Session struct {
	SessionID           string
	Scopes              []string
	OIDCNonce           string
	User                User
	Granted             bool
	CodeChallenge       string
	CodeChallengeMethod string
}

Session stores a User and their OIDC options across requests

func (*Session) AccessToken

func (s *Session) AccessToken(config *Config, kp *Keypair, now time.Time) (string, error)

AccessToken returns the JWT token with the appropriate claims for an access token

func (*Session) IDToken

func (s *Session) IDToken(config *Config, kp *Keypair, now time.Time) (string, error)

IDToken returns the JWT token with the appropriate claims for a user based on the scopes set.

func (*Session) RefreshToken

func (s *Session) RefreshToken(config *Config, kp *Keypair, now time.Time) (string, error)

RefreshToken returns the JWT token with the appropriate claims for a refresh token

type SessionStore

type SessionStore struct {
	Store     map[string]*Session
	CodeQueue *CodeQueue
}

SessionStore manages our Session objects

func NewSessionStore

func NewSessionStore() *SessionStore

NewSessionStore initializes the SessionStore for this server

func (*SessionStore) GetSessionByID

func (ss *SessionStore) GetSessionByID(id string) (*Session, error)

GetSessionByID looks up the Session

func (*SessionStore) GetSessionByToken

func (ss *SessionStore) GetSessionByToken(token *jwt.Token) (*Session, error)

GetSessionByToken decodes a token and looks up a Session based on the session ID claim.

func (*SessionStore) NewSession

func (ss *SessionStore) NewSession(scope string, nonce string, user User, codeChallenge string, codeChallengeMethod string) (*Session, error)

NewSession creates a new Session for a User

type TimeReset

type TimeReset func()

TimeReset is a function that resets time

type User

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

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 UserQueue

type UserQueue struct {
	sync.Mutex
	Queue []User
}

UserQueue manages the queue of Users returned for each call to the authorize endpoint

func (*UserQueue) Pop

func (q *UserQueue) Pop() User

Pop a User from the Queue. If empty, return `DefaultUser()`

func (*UserQueue) Push

func (q *UserQueue) Push(user User)

Push adds a User to the Queue to be set in subsequent calls to the `authorization_endpoint`

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL