testing

package
v0.66.0 Latest Latest
Warning

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

Go to latest
Published: Sep 19, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package testing provides an in-memory fake JWT issuer for exercising the auth package's bearer/JWT verifier without a real identity provider.

The primary type is Issuer, which holds one or more RSA key pairs and mints compact JWS credentials — valid ones plus every rejection shape a verifier must refuse (expired, wrong audience, wrong issuer, unknown kid, alg=none, ES256, …). Feed Issuer.PublicKeys() to a static key resolver and the verifier will accept exactly what this issuer signs.

Minting never returns an error: a fake that forces error plumbing at every call site reads worse than one that panics on the impossible. Key generation failure and a payload that cannot be JSON-encoded (a channel in Claims.Extra, say) panic with a clear message; nothing else can fail.

Example usage:

iss := NewIssuer().WithIssuerURL("https://idp.test/").WithAudience("payments-api")
token := iss.Mint(Claims{Subject: "user-42", Extra: map[string]any{"scope": "read"}})
expired := iss.MintExpired()

Index

Constants

View Source
const (
	DefaultIssuerURL = "https://issuer.test/"
	DefaultAudience  = "go-bricks-test"
	DefaultKeyID     = "test-key-1"
	DefaultSubject   = "test-subject"
	DefaultLifetime  = time.Hour
)

Defaults applied by NewIssuer when a builder does not override them.

View Source
const (
	UnknownKeyID  = "unknown-kid"
	WrongAudience = "wrong-audience"
	WrongIssuer   = "https://attacker.test/"
)

Values used by the negative-case minting helpers. They are exported so a test can assert that a rejection names the offending value.

View Source
const DefaultOversizedBytes = 64 * 1024

DefaultOversizedBytes is the filler JWKSOversized adds. A test caps the consumer below it — the framework's auth.jwt.jwks.maxbodybytes — rather than raising this.

View Source
const JWKSPath = "/.well-known/jwks.json"

JWKSPath is the path the fake endpoint serves the key set on. Any other path is answered identically, so a consumer's URL only has to point at the server.

View Source
const JWKSRedirectPath = "/redirect/jwks.json"

JWKSRedirectPath is the path that answers with a redirect once SetRedirectLocation has set one, so a test can point a consumer at RedirectURL and drive its redirect policy. Until then it serves the key set like any other path.

Variables

This section is empty.

Functions

This section is empty.

Types

type Algorithm

type Algorithm string

Algorithm names the JWS signature algorithm written to the protected header.

const (
	AlgRS256 Algorithm = "RS256"
	AlgPS256 Algorithm = "PS256"
	AlgES256 Algorithm = "ES256"
	AlgNone  Algorithm = "none"
)

Algorithms the issuer can mint. AlgNone and AlgES256 exist only to prove a verifier refuses them: the framework's allowlist is {RS256, PS256}.

type Claims

type Claims struct {
	Subject    string
	Issuer     string
	Audience   []string
	IssuedAt   time.Time
	NotBefore  time.Time
	ExpiresAt  time.Time
	OmitExpiry bool
	// Extra is merged into the payload before the standard claims, so a standard
	// claim always wins over a same-named extra.
	Extra map[string]any
}

Claims describes the JWT payload to mint. Zero fields take issuer defaults: Subject becomes DefaultSubject, Issuer the issuer URL, Audience the default audience, IssuedAt the current time, and ExpiresAt one DefaultLifetime ahead. NotBefore is omitted when zero.

A single-element Audience is encoded as a JSON string and a multi-element one as an array, matching what real issuers emit.

type Issuer

type Issuer struct {
	// contains filtered or unexported fields
}

Issuer is an in-memory JWT issuer holding RSA key pairs under stable key IDs. It is not safe for concurrent rotation, but minting from several goroutines is fine once configuration has settled.

func NewIssuer

func NewIssuer() *Issuer

NewIssuer returns an issuer with one RSA-2048 key pair under DefaultKeyID. The unexposed key pair behind MintUnknownKeyID/MintBadSignature and the ECDSA key behind MintES256 are generated lazily on first use. It panics if key generation fails.

func (*Issuer) ActiveKeyID

func (i *Issuer) ActiveKeyID() string

ActiveKeyID returns the kid credentials are currently signed under.

func (*Issuer) Audience

func (i *Issuer) Audience() string

Audience returns the configured default `aud` value.

func (*Issuer) IssuerURL

func (i *Issuer) IssuerURL() string

IssuerURL returns the configured `iss` value.

func (*Issuer) Mint

func (i *Issuer) Mint(claims Claims) string

Mint returns a compact JWS over claims, signed RS256 with the active key.

func (*Issuer) MintAlgNone

func (i *Issuer) MintAlgNone() string

MintAlgNone returns an unsigned credential with alg=none and an empty signature segment, hand-assembled because go-jose refuses to produce one.

func (*Issuer) MintBadSignature

func (i *Issuer) MintBadSignature() string

MintBadSignature returns a credential carrying the active kid but signed with a key pair the verifier does not hold.

func (*Issuer) MintCorruptSignature

func (i *Issuer) MintCorruptSignature() string

MintCorruptSignature returns a valid credential whose signature bytes have one bit flipped, so the wire shape stays well-formed but the signature does not verify. The segment is decoded, the low bit of a middle byte inverted and the result re-encoded: unconditional, so the corruption is guaranteed rather than dependent on what the signature happened to contain.

func (*Issuer) MintES256

func (i *Issuer) MintES256() string

MintES256 returns a credential signed with an ECDSA P-256 key, proving the verifier refuses a non-RSA algorithm.

func (*Issuer) MintExpired

func (i *Issuer) MintExpired() string

MintExpired returns a credential that expired half an hour ago, with an `iat` two hours in the past — MintExpiredWithin(time.Hour). The verifier caps leeway at five minutes, so half an hour is always past it.

func (*Issuer) MintExpiredWithin

func (i *Issuer) MintExpiredWithin(leeway time.Duration) string

MintExpiredWithin returns a credential that expired half of leeway ago, so a verifier configured with that leeway still accepts it and one without rejects it. Its `iat` is two leeways in the past, so the issued-in-future rule cannot fire first.

func (*Issuer) MintFutureIssuedAt

func (i *Issuer) MintFutureIssuedAt() string

MintFutureIssuedAt returns a credential whose `iat` is an hour in the future.

func (*Issuer) MintFutureNotBefore

func (i *Issuer) MintFutureNotBefore() string

MintFutureNotBefore returns a credential whose `nbf` is an hour in the future. Its `exp` takes the default one DefaultLifetime ahead, so the window never opens — the helper exists to exercise the not-yet-valid rule, not to become valid later.

func (*Issuer) MintMissingExpiry

func (i *Issuer) MintMissingExpiry() string

MintMissingExpiry returns a credential with no `exp` claim.

func (*Issuer) MintMissingKeyID

func (i *Issuer) MintMissingKeyID() string

MintMissingKeyID returns a credential with no kid header.

func (*Issuer) MintPS256

func (i *Issuer) MintPS256() string

MintPS256 returns a valid credential signed with PS256 instead of RS256.

func (*Issuer) MintUnknownKeyID

func (i *Issuer) MintUnknownKeyID() string

MintUnknownKeyID returns a credential whose kid header (UnknownKeyID) names a key the verifier will not have; it is signed with an unexposed key pair.

func (*Issuer) MintWith

func (i *Issuer) MintWith(opts MintOptions) string

MintWith is the flexible entry point every other minting helper delegates to.

func (*Issuer) MintWithType

func (i *Issuer) MintWithType(typ string) string

MintWithType returns a valid credential whose typ header is typ, e.g. "at+jwt".

func (*Issuer) MintWrongAudience

func (i *Issuer) MintWrongAudience() string

MintWrongAudience returns a credential addressed to WrongAudience.

func (*Issuer) MintWrongIssuer

func (i *Issuer) MintWrongIssuer() string

MintWrongIssuer returns a credential whose `iss` is WrongIssuer.

func (*Issuer) PublicKey

func (i *Issuer) PublicKey(kid string) *rsa.PublicKey

PublicKey returns a deep copy of the public key registered under kid, or nil.

func (*Issuer) PublicKeys

func (i *Issuer) PublicKeys() map[string]*rsa.PublicKey

PublicKeys returns a copy of every public key the issuer will sign with, keyed by kid — ready to hand to a static key resolver.

Each key is deep-copied: the stored value is the public half of the private key MintWith signs with, so handing out that pointer would let a caller corrupt later signing by writing to the modulus.

func (*Issuer) Rotate

func (i *Issuer) Rotate(kid string) *Issuer

Rotate adds a fresh RSA key pair under kid and makes it the active signing key. Previously issued credentials stay verifiable because the old public key remains in PublicKeys.

An empty kid panics: it would make every subsequent Mint emit an empty kid header, which a verifier refuses as a missing key ID — use MintMissingKeyID to omit the header deliberately. An already-registered kid panics too: overwriting a key would silently break every credential minted under it, which is the opposite of what this helper promises and would make a rotation test assert the wrong thing.

func (*Issuer) WithAudience

func (i *Issuer) WithAudience(audience string) *Issuer

WithAudience sets the `aud` claim minted by default.

func (*Issuer) WithClock

func (i *Issuer) WithClock(now func() time.Time) *Issuer

WithClock replaces the time source used for the default iat/exp claims, making minted lifetimes deterministic.

func (*Issuer) WithIssuerURL

func (i *Issuer) WithIssuerURL(url string) *Issuer

WithIssuerURL sets the `iss` claim minted by default.

type JWKSMode

type JWKSMode int

JWKSMode selects what the fake key set endpoint serves. It exists so a test can drive a verifier through the failure modes a real issuer exhibits — outage, garbage, and a body too large to read — without a proxy.

const (
	// JWKSHealthy serves the issuer's current public keys as a JWKS document.
	JWKSHealthy JWKSMode = iota
	// JWKSServerError answers every request with 503 and an empty body.
	JWKSServerError
	// JWKSMalformed answers 200 with a body that is not a JWKS document.
	JWKSMalformed
	// JWKSOversized answers 200 with a well-formed document padded past
	// DefaultOversizedBytes, to exercise the consumer's body cap.
	JWKSOversized
	// JWKSNonRSAOnly answers 200 with a well-formed document carrying only the
	// entries added through AddECKey/AddRawKey — no RSA key at all. It is the
	// shape an RSA-only consumer must treat as a failed fetch rather than as an
	// empty key set.
	JWKSNonRSAOnly
)

type JWKSRequest

type JWKSRequest struct {
	// At is when the handler received the request, on the real clock. It orders
	// the log; a test asserting refresh behavior should assert on COUNT and
	// order, never on elapsed time.
	At time.Time
	// Method and Path are taken verbatim from the request line.
	Method string
	Path   string
}

JWKSRequest is one recorded request to the key set endpoint.

type JWKSServer

type JWKSServer struct {
	// contains filtered or unexported fields
}

JWKSServer is the network half of the fake issuer: a TLS httptest server publishing Issuer's public keys as a JWKS document, with a settable failure mode and a request log.

The request log is what a refresh test asserts on. Refresh behavior — one coalesced fetch per unknown kid, no second fetch inside the minimum interval — is observable as a request COUNT, so a test never has to sleep for it.

Every method is safe for concurrent use, and rotation performed through Rotate is serialized against the handler, so a key added mid-test is published without racing a concurrent fetch.

func NewJWKSServer

func NewJWKSServer(issuer *Issuer) *JWKSServer

NewJWKSServer starts a TLS server publishing issuer's public keys. The caller must Close it.

The certificate is httptest's own, so a consumer must dial through HTTPClient — an httpclient built over any other transport will fail the handshake, which is the point: the key set is the verifier's trust anchor and the framework refuses a plaintext endpoint.

func (*JWKSServer) AddECKey

func (s *JWKSServer) AddECKey(kid string)

AddECKey publishes the issuer's ECDSA P-256 public key under kid, so the document carries a key the RSA-only consumer must drop. The kid is the one a dropped-keys warning names.

func (*JWKSServer) AddRawKey

func (s *JWKSServer) AddRawKey(entry any)

AddRawKey publishes an arbitrary JWK entry alongside the issuer's RSA keys, for a shape the typed helpers do not cover.

func (*JWKSServer) Close

func (s *JWKSServer) Close()

Close shuts the server down. It is safe to call more than once only through the usual t.Cleanup discipline; httptest panics on a double close.

func (*JWKSServer) HTTPClient

func (s *JWKSServer) HTTPClient() *nethttp.Client

HTTPClient returns an http.Client that trusts this server's certificate. Pass it to httpclient's WithHTTPClient to reach the endpoint.

func (*JWKSServer) Issuer

func (s *JWKSServer) Issuer() *Issuer

Issuer returns the issuer whose keys this server publishes.

func (*JWKSServer) RedirectURL

func (s *JWKSServer) RedirectURL() string

RedirectURL is the endpoint that answers with the location SetRedirectLocation set, as an https URL suitable for auth.jwt.jwksuri.

func (*JWKSServer) RequestCount

func (s *JWKSServer) RequestCount() int

RequestCount is the number of requests served so far.

func (*JWKSServer) Requests

func (s *JWKSServer) Requests() []JWKSRequest

Requests returns a copy of the request log, oldest first.

func (*JWKSServer) ResetRequests

func (s *JWKSServer) ResetRequests()

ResetRequests clears the request log, so a test can count the requests one phase makes without subtracting the previous phase's.

func (*JWKSServer) Rotate

func (s *JWKSServer) Rotate(kid string)

Rotate adds a fresh key under kid to the issuer and publishes it, serialized against in-flight requests. It is Issuer.Rotate's race-free form and panics on the same inputs.

func (*JWKSServer) SetMode

func (s *JWKSServer) SetMode(mode JWKSMode)

SetMode changes what the endpoint serves, from the next request on.

func (*JWKSServer) SetOversizedBytes

func (s *JWKSServer) SetOversizedBytes(n int)

SetOversizedBytes sets the filler JWKSOversized adds, in bytes; the served document is that much larger than the real one.

It panics on a negative n, before taking the lock: the value is only used inside the handler, where strings.Repeat would panic while the lock is held and leave every later request and mutator blocked forever. Rejecting it here fails the test that set it, in the shape the rest of this package panics.

func (*JWKSServer) SetRedirectLocation

func (s *JWKSServer) SetRedirectLocation(location string)

SetRedirectLocation makes JWKSRedirectPath answer 302 to location, from the next request on. An empty location restores the key set response, so a test can end a chain on a document. The redirect hop is recorded in the request log like any other request.

func (*JWKSServer) URL

func (s *JWKSServer) URL() string

URL is the key set endpoint, an https URL suitable for auth.jwt.jwksuri.

type MintOptions

type MintOptions struct {
	Claims
	// Algorithm defaults to AlgRS256.
	Algorithm Algorithm
	// KeyID overrides the header kid; empty means the active signing kid.
	KeyID string
	// OmitKeyID drops the kid header entirely. It wins over KeyID.
	OmitKeyID bool
	// Type overrides the typ header, which defaults to "JWT".
	Type string
	// SignKey signs with a caller-supplied key instead of the issuer's — an
	// *rsa.PrivateKey or an *ecdsa.PrivateKey. Used to mint a well-formed
	// credential the verifier cannot validate.
	SignKey any
}

MintOptions is the flexible minting entry point's input: the payload plus the header and signing choices.

Jump to

Keyboard shortcuts

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