Documentation
¶
Overview ¶
Package auth implements the gateway's OAuth 2.0 resource server (Bearer JWT validation against an issuer allowlist via cached JWKS, with exact audience matching) and the upstream credential strategies.
Index ¶
- func ScopesFromClaims(claims map[string]any) []string
- func TrustAnchorClient() *http.Client
- func WithPrincipal(ctx context.Context, p *Principal) context.Context
- type EMA
- func (m *EMA) AuthorizationServerMetadata() map[string]any
- func (m *EMA) AuthorizationServerMetadataPaths() []string
- func (m *EMA) Issuer() string
- func (m *EMA) PublicKey() *ecdsa.PublicKey
- func (m *EMA) ServeAuthorizationServerMetadata(w http.ResponseWriter, _ *http.Request)
- func (m *EMA) ServeJWKS(w http.ResponseWriter, _ *http.Request)
- func (m *EMA) ServeToken(w http.ResponseWriter, r *http.Request)
- type Principal
- type TokenExchange
- type UpstreamCredentials
- type Verifier
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ScopesFromClaims ¶ added in v1.15.0
ScopesFromClaims reads the OAuth scopes a verified token carries.
There is no single spelling to read, so both in use are accepted. RFC 6749 §3.3 and RFC 9068 §2.2.3 make "scope" a *space-delimited string*, which is why a policy could not previously express a scope requirement at all: the claims matcher compares whole values, and "write" is not equal to "read write admin". Entra spells the same thing "scp", and some issuers send either as a JSON array. All four shapes are read here so a rule can be written once against the concept rather than against an issuer's spelling.
"scope" is read first and "scp" only when it yields nothing, rather than merging the two: an issuer that sends both sends the same set twice, and merging would silently union two claims that disagreed.
func TrustAnchorClient ¶ added in v1.11.0
TrustAnchorClient returns the HTTP client fold uses to fetch trust-anchor documents — issuer JWKS sets and the EMA IdP's JWKS. It matches the posture of every other outbound trust-path client (discovery, token endpoints, audit webhooks): a bounded timeout so a slow IdP cannot pin verification goroutines forever, and redirects refused outright — a redirect is never a legitimate step in fetching a key set, and following one would let whoever answers the configured URI hand the fetch to a host of their choosing.
http.DefaultClient has neither property; never wire it here.
Types ¶
type EMA ¶
type EMA struct {
// OnExchange, when set, is told the outcome of every token exchange —
// including the refusals. The gateway wires it into the audit trail:
// this endpoint is an authorization server, and a detected ID-JAG
// replay, or an attacker fuzzing assertions under the rate limit, is
// exactly the traffic a SIEM exists to see. Set before serving; not
// synchronized after.
OnExchange func(TokenExchange)
// contains filtered or unexported fields
}
EMA is fold's embedded MCP Authorization Server, deliberately one grant wide: exchange an enterprise-IdP-issued ID-JAG (Identity Assertion JWT Authorization Grant) for a short-lived fold-signed access token (Enterprise-Managed Authorization). Everything the gateway later accepts has aud = fold, which keeps upstream token exchange coherent.
func NewEMA ¶
NewEMA loads the ES256 signing key named by signingKeyRef (a PKCS#8 PEM in an environment variable) and assembles the exchange service.
func (*EMA) AuthorizationServerMetadata ¶ added in v1.15.0
AuthorizationServerMetadata is the RFC 8414 document for the embedded authorization server.
fold lists itself in `authorization_servers` of its RFC 9728 protected- resource metadata whenever EMA is on, which tells a client to discover this document — so not serving it left the advertisement pointing at a 404 and forced every operator to configure the token endpoint out of band. The document is deliberately narrow: it describes the one grant this server implements and claims nothing else.
func (*EMA) AuthorizationServerMetadataPaths ¶ added in v1.15.0
AuthorizationServerMetadataPaths returns every well-known path this document must be reachable at.
RFC 8414 §3.1 locates the document by inserting the well-known segment *before* the issuer's path, so an issuer with a path is discovered at "/.well-known/oauth-authorization-server/{path}" rather than at the root. fold advertises itself in the RFC 9728 document using whatever auth.resource says, so whichever form that takes has to resolve — hence both, when they differ.
func (*EMA) ServeAuthorizationServerMetadata ¶ added in v1.15.0
func (m *EMA) ServeAuthorizationServerMetadata(w http.ResponseWriter, _ *http.Request)
ServeAuthorizationServerMetadata answers the RFC 8414 well-known paths.
CORS mirrors what the SDK sets on the protected-resource document: a browser-based MCP client discovers this from script, so without the header it can read fold's RFC 9728 metadata and then fail on the authorization server it points at.
func (*EMA) ServeJWKS ¶
func (m *EMA) ServeJWKS(w http.ResponseWriter, _ *http.Request)
ServeJWKS answers GET /.well-known/jwks.json with the public key set.
func (*EMA) ServeToken ¶
func (m *EMA) ServeToken(w http.ResponseWriter, r *http.Request)
ServeToken answers POST /oauth/token — the ID-JAG exchange. The caller is unauthenticated by design (the assertion is the credential); the gateway rate-limits this handler before it runs. Every terminal response reports through OnExchange — refusals included — so the trail this endpoint produces matches the single-exit-door rule the rest of the gateway obeys.
type Principal ¶
type Principal struct {
Subject string // "sub" claim
Issuer string // "iss" claim
Groups []string // from the issuer's configured groups claim
Scopes []string // OAuth scopes, from the "scope" or "scp" claim
Token string // the raw bearer token (for passthrough / token-exchange)
Expiry time.Time // token expiration
// Claims is the verified token's full claim set, for attribute-based
// policy (policy subjects' "claims" matcher). Values are as decoded
// from JSON: string, float64, bool, nil, []any, map[string]any.
Claims map[string]any
}
Principal is the authenticated caller of a gateway request.
func PrincipalFromContext ¶
PrincipalFromContext returns the authenticated principal, or nil when the gateway runs with auth disabled.
type TokenExchange ¶ added in v1.11.0
type TokenExchange struct {
// Outcome is "minted" for a successful exchange, "replayed" for an
// ID-JAG presented twice (the security event), or the OAuth error code
// returned otherwise ("invalid_request", "unsupported_grant_type",
// "invalid_grant", "server_error").
Outcome string
Detail string // the error_description sent to the caller ("" when minted)
Subject string // the assertion's subject, when validation got that far
Issuer string // the IdP issuer the assertion was verified against
}
TokenExchange describes one terminal /oauth/token response.
type UpstreamCredentials ¶
type UpstreamCredentials struct {
// contains filtered or unexported fields
}
UpstreamCredentials attaches credentials to requests bound for one upstream, according to its configured strategy:
none — nothing attached
static — an API key from the secret store (env vars)
passthrough — the caller's bearer token, forwarded as-is
client-credentials — a service-identity token (OAuth 2.0 CC grant),
cached until 60s before expiry
token-exchange — RFC 8693: the caller's token exchanged for an
upstream-audience token, cached per subject
func NewUpstreamCredentials ¶
func NewUpstreamCredentials(cfg *config.UpstreamAuth, client *http.Client) *UpstreamCredentials
NewUpstreamCredentials builds the credential injector for one upstream. A nil cfg (or strategy "none") attaches nothing.
The caller's client is wrapped so token-endpoint requests never follow a redirect. Those requests carry the most sensitive material fold handles — the client secret under client-credentials, and the caller's own bearer token as subject_token under token-exchange — and Go replays a POST body verbatim on 307/308, so a token endpoint that redirects (compromised, misconfigured, or hosting an open redirect) would otherwise hand both to whatever host it names. Refusing every redirect is safe here: token endpoints answer 200 with the token, and a redirect is never a legitimate step in the grant.
func (*UpstreamCredentials) Apply ¶
Apply sets credential headers on hdr for a request running under ctx.
func (*UpstreamCredentials) PerRequest ¶
func (c *UpstreamCredentials) PerRequest() bool
PerRequest reports whether this strategy derives credentials from the caller (so they must be attached per request, not per session).
type Verifier ¶
type Verifier struct {
// contains filtered or unexported fields
}
Verifier validates gateway bearer tokens: trusted issuer (checked before any network I/O), signature via cached JWKS, exact audience match.
func NewVerifier ¶
NewVerifier builds a verifier from the auth config section. Only "direct" issuers are trusted for straight token presentation: an "exchange" issuer's tokens (ID-JAGs) must go through the EMA exchange — accepting one directly would let it stand in for a fold access token.
func (*Verifier) TrustLocal ¶
TrustLocal registers an issuer whose tokens verify against a locally held public key — fold's own EMA-minted tokens — with no JWKS fetch.