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
- type Algorithm
- type Claims
- type Issuer
- func (i *Issuer) ActiveKeyID() string
- func (i *Issuer) Audience() string
- func (i *Issuer) IssuerURL() string
- func (i *Issuer) Mint(claims Claims) string
- func (i *Issuer) MintAlgNone() string
- func (i *Issuer) MintBadSignature() string
- func (i *Issuer) MintCorruptSignature() string
- func (i *Issuer) MintES256() string
- func (i *Issuer) MintExpired() string
- func (i *Issuer) MintExpiredWithin(leeway time.Duration) string
- func (i *Issuer) MintFutureIssuedAt() string
- func (i *Issuer) MintFutureNotBefore() string
- func (i *Issuer) MintMissingExpiry() string
- func (i *Issuer) MintMissingKeyID() string
- func (i *Issuer) MintPS256() string
- func (i *Issuer) MintUnknownKeyID() string
- func (i *Issuer) MintWith(opts MintOptions) string
- func (i *Issuer) MintWithType(typ string) string
- func (i *Issuer) MintWrongAudience() string
- func (i *Issuer) MintWrongIssuer() string
- func (i *Issuer) PublicKey(kid string) *rsa.PublicKey
- func (i *Issuer) PublicKeys() map[string]*rsa.PublicKey
- func (i *Issuer) Rotate(kid string) *Issuer
- func (i *Issuer) WithAudience(audience string) *Issuer
- func (i *Issuer) WithClock(now func() time.Time) *Issuer
- func (i *Issuer) WithIssuerURL(url string) *Issuer
- type JWKSMode
- type JWKSRequest
- type JWKSServer
- func (s *JWKSServer) AddECKey(kid string)
- func (s *JWKSServer) AddRawKey(entry any)
- func (s *JWKSServer) Close()
- func (s *JWKSServer) HTTPClient() *nethttp.Client
- func (s *JWKSServer) Issuer() *Issuer
- func (s *JWKSServer) RedirectURL() string
- func (s *JWKSServer) RequestCount() int
- func (s *JWKSServer) Requests() []JWKSRequest
- func (s *JWKSServer) ResetRequests()
- func (s *JWKSServer) Rotate(kid string)
- func (s *JWKSServer) SetMode(mode JWKSMode)
- func (s *JWKSServer) SetOversizedBytes(n int)
- func (s *JWKSServer) SetRedirectLocation(location string)
- func (s *JWKSServer) URL() string
- type MintOptions
Constants ¶
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.
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.
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.
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.
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.
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 ¶
ActiveKeyID returns the kid credentials are currently signed under.
func (*Issuer) MintAlgNone ¶
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 ¶
MintBadSignature returns a credential carrying the active kid but signed with a key pair the verifier does not hold.
func (*Issuer) MintCorruptSignature ¶
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 ¶
MintES256 returns a credential signed with an ECDSA P-256 key, proving the verifier refuses a non-RSA algorithm.
func (*Issuer) MintExpired ¶
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 ¶
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 ¶
MintFutureIssuedAt returns a credential whose `iat` is an hour in the future.
func (*Issuer) MintFutureNotBefore ¶
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 ¶
MintMissingExpiry returns a credential with no `exp` claim.
func (*Issuer) MintMissingKeyID ¶
MintMissingKeyID returns a credential with no kid header.
func (*Issuer) MintUnknownKeyID ¶
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 ¶
MintWithType returns a valid credential whose typ header is typ, e.g. "at+jwt".
func (*Issuer) MintWrongAudience ¶
MintWrongAudience returns a credential addressed to WrongAudience.
func (*Issuer) MintWrongIssuer ¶
MintWrongIssuer returns a credential whose `iss` is WrongIssuer.
func (*Issuer) PublicKey ¶
PublicKey returns a deep copy of the public key registered under kid, or nil.
func (*Issuer) PublicKeys ¶
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 ¶
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 ¶
WithAudience sets the `aud` claim minted by default.
func (*Issuer) WithClock ¶
WithClock replaces the time source used for the default iat/exp claims, making minted lifetimes deterministic.
func (*Issuer) WithIssuerURL ¶
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.