Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ The same reasoning applies to the DPoP header specifically:
request carried, not a single already-resolved string — a caller
passes `http.Header.Values("DPoP")` directly, never `Header.Get`, which
silently returns only the first of several values instead of letting
the request be rejected. RFC 9449 §7.1 requires a request carry at
the request be rejected. RFC 9449 §4.3 (check 1) requires a request carry at
most one DPoP header; `internal/dpop.ResolveHeaderValues` (used by both
`server` and `resource`) enforces that itself, rejecting more than one
value regardless of whether any individual value would otherwise have
Expand Down Expand Up @@ -536,7 +536,7 @@ specific DCR plan) has no DCR module at all. If it is added:
- `RegisterClient`/`ReadClient`/`UpdateClient`/`DeleteClient` follow
the same typed-request → typed-result/typed-error shape every other
method here does — no generic `HandleRequest` for this either.
- Registration access tokens (RFC 7592 §1.1) are a new storage
- Registration access tokens (RFC 7592 §1.2) are a new storage
primitive, not a repurposed `GrantStore` refresh token: they
authenticate the management endpoint itself, not a resource, and
their rotate-on-read/update semantics (RFC 7592 §5, a MAY, never a
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ implemented. `server` only ever accepts `response_type=code`, and
`ClientAuthMethod` is a closed enum of `private_key_jwt`, the mTLS
variants and OAuth 2.0 attestation-based client authentication.

- FAPI 2.0 Security Profile Final + Message Signing Final
- FAPI 2.0 Security Profile Final + Message Signing Final (signed requests with JAR, signed responses with JARM; signed introspection responses aren't implemented)
- PAR (RFC 9126) · DPoP (RFC 9449) · mTLS client auth & cert-bound tokens (RFC 8705), with CRL revocation checking
- private_key_jwt client authentication
- OAuth 2.0 Attestation-Based Client Authentication, including HAIP 1.0 x5c attester certificate chains, optionally binding each trust anchor to the attesters it may vouch for
Expand Down
6 changes: 3 additions & 3 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -707,9 +707,9 @@ parameter requesting a specific `sub` value, such as
`{"id_token":{"sub":{"value":"248289761001"}}}`, to the pushed
authorization request or the CIBA backchannel authentication endpoint.

**Why:** OIDC Core §5.5.1 says that when a client requests a specific
`sub`, the server MUST NOT reply with an ID token or access token for a
different user, and `id_token_hint` (§3.1.2.1, CIBA Core §7.1) names
**Why:** OIDC Core §3.1.2.2 says the server MUST NOT reply with an ID
token or access token for a different user than a requested `sub`
names (§5.5.1), and `id_token_hint` (§3.1.2.1, CIBA Core §7.1) names
the user the same way. The pushed authorization request dropped
`id_token_hint` as an unregistered parameter, CIBA passed it to the
application unverified, and a `sub` value was never read, so a request
Expand Down
12 changes: 10 additions & 2 deletions client/begin_authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@ const errPushedAuthorizationRequestFailed = "pushed authorization request failed
// authenticates to and calls the
// pushed-authorization-request endpoint (RFC 9126), and persists
// correlation state for the eventual callback.
//
// Call it only from a request the user made on purpose and that is
// protected against cross-site request forgery: a POST guarded by a
// SameSite cookie or a CSRF token, not a plain GET any page can trigger.
// An endpoint that starts an authorization on any cross-site request
// lets another site begin flows in the user's browser, and a user
// already signed in at the authorization server (who may have consented
// before) can then end up granting access without meaning to.
func (c *Client) BeginAuthorization(ctx context.Context, req BeginAuthorizationRequest) (AuthorizationSession, error) {
redirectURI, reqErr := c.checkBeginRequest(req)
if reqErr != nil {
Expand Down Expand Up @@ -403,8 +411,8 @@ func (c *Client) pushAuthorizationRequestWithJKT(ctx context.Context, params map

// pushAuthorizationRequestWithDPoPProof implements PARDPoPBindingProof
// (the default): binds the authorization code to this client's DPoP key
// by presenting an actual proof at PAR — RFC 9449 §10.1's recommended
// mechanism — instead of the plain dpop_jkt parameter, retrying once on
// by presenting an actual proof at PAR — the stronger binding of RFC
// 9449 §10.1's two mechanisms — instead of the plain dpop_jkt parameter, retrying once on
// a use_dpop_nonce or attestation challenge (see challengeRetry). This mirrors sendTokenRequest's identical
// mechanic for the token endpoint: an authorization server that
// nonce-challenges DPoP proofs it verifies can now challenge this one
Expand Down
15 changes: 8 additions & 7 deletions client/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,19 @@ const (
// both PAR and DPoP must accept either). Unlike Profile/Algorithms/
// Limits, whose zero value is deliberately invalid because there's no
// universally-preferable choice, this type's zero value is a real,
// meaningful default: RFC 9449 §10.1 itself recommends
// PARDPoPBindingProof over PARDPoPBindingJKT, so a Config that never
// mentions this field gets the recommended behavior for free rather
// than a validation error.
// meaningful default: of the two mechanisms RFC 9449 §10.1 has a server
// support, the DPoP header "provides a stronger binding", so a Config
// that never mentions this field gets PARDPoPBindingProof rather than a
// validation error.
type PARDPoPBinding uint8

const (
// PARDPoPBindingProof (the default, zero value) sends an actual DPoP
// proof — not just its key's thumbprint — as this pushed
// authorization request's own "DPoP" header, binding the eventual
// authorization code to whichever key demonstrated possession here.
// RFC 9449 §10.1 recommends this over PARDPoPBindingJKT: it reuses
// RFC 9449 §10.1 notes it "provides a stronger binding" than
// PARDPoPBindingJKT's dpop_jkt, and it reuses
// the same proof-building this client already does at the token and
// resource endpoints (no separate thumbprint computation or
// parameter), and unlike a bare dpop_jkt claim, it's actual proof of
Expand Down Expand Up @@ -457,8 +458,8 @@ type Config struct {

// PARDPoPBinding selects how BeginAuthorization commits this
// client's DPoP key at PAR time — see PARDPoPBinding's own doc
// comment. Defaults to PARDPoPBindingProof, RFC 9449 §10.1's own
// recommended mechanism. Only meaningful when SenderConstrain is
// comment. Defaults to PARDPoPBindingProof, the stronger binding of
// RFC 9449 §10.1's two. Only meaningful when SenderConstrain is
// SenderConstrainDPoP; ignored entirely under SenderConstrainMTLS,
// which has no PAR-time pre-commitment concept of its own (RFC 8705
// binding is derived purely from whichever certificate authenticates
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/mtls.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ srv, err := server.New(server.Config{
// Issuer, Endpoints, Profile, Algorithms, Limits, Subjects ...
MTLSEndpoints: server.MTLSEndpoints{Token: mtlsTokenURL},
}, server.Dependencies{
// RFC 8705 §2.1.1: verify the chain yourself, and check revocation.
// RFC 8705 §2.1: verify the chain yourself, and check revocation.
ClientCertificateTrust: server.TrustedClientCAs{
Roots: rootCAs,
Intermediates: issuingCAs, // revocation-checked, unlike Roots
Expand Down
2 changes: 1 addition & 1 deletion encryption_algorithm.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const (
// the CBC-HMAC family requires.
A256GCM

// A256CBCHS512 is AES_256_CBC_HMAC_SHA_512 (RFC 7518 §5.2.3): a
// A256CBCHS512 is AES_256_CBC_HMAC_SHA_512 (RFC 7518 §5.2.5): a
// 64-octet CEK split into a 32-octet HMAC-SHA-512 key and a
// 32-octet AES-256 key, PKCS #7-padded CBC encryption under a
// 128-bit IV, and an authentication tag computed as HMAC-SHA-512
Expand Down
4 changes: 3 additions & 1 deletion federation/trustmark.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,9 @@ func (r *Resolver) verifyTrustMarkDelegationAgainstJWKS(d intfed.TrustMarkDelega

// CheckTrustMarkStatus queries trustMarkToken's own issuer for its
// current status (OpenID Federation 1.0 §8.4) and returns the verified
// response claims. §8.4: "The query MUST be sent to the Trust Mark
// response claims. It says only whether the issuer still considers the
// mark active, not that the mark is valid: call it after VerifyTrustMark
// has accepted the mark, never instead of it. §8.4: "The query MUST be sent to the Trust Mark
// Issuer" — trustMarkToken's own unverified "iss" claim names it, and
// that issuer is resolved as a fresh Trust Chain (the same way
// VerifyTrustMark already establishes trust in a Trust Mark's issuer)
Expand Down
2 changes: 1 addition & 1 deletion federation/trustmark_issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ type DelegationParams struct {
TrustMarkType string

// Lifetime bounds how long the delegation is valid for. Zero omits
// the "exp" claim entirely — OpenID Federation 1.0 §7.2: "If not
// the "exp" claim entirely — OpenID Federation 1.0 §7.2.1: "If not
// present, it means that the delegation does not expire."
Lifetime time.Duration
}
Expand Down
4 changes: 2 additions & 2 deletions internal/dpop/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package dpop

// ResolveHeaderValues reduces values — every "DPoP" HTTP header value a
// request carried, in receipt order — to the single proof value this
// package's own verification expects, enforcing RFC 9449 §7.1 itself
// ("the client... MUST NOT send this header field multiple times")
// package's own verification expects, enforcing RFC 9449 §4.3 check 1
// itself ("There is not more than one DPoP HTTP request header field")
// rather than leaving that check to whichever HTTP adapter a caller
// happens to use. net/http's own Header.Get silently returns only the
// first of several values instead of rejecting the request, so a
Expand Down
2 changes: 1 addition & 1 deletion internal/federation/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ var (

// ErrTrustMarkDelegationWrongType indicates a Trust Mark Delegation
// JWT's "typ" header was not "trust-mark-delegation+jwt" (OpenID
// Federation 1.0 §7.2's own "Trust Mark delegation JWTs without a
// Federation 1.0 §7.2.1's "Trust Mark delegation JWTs without a
// typ header parameter or with a different typ value MUST be
// rejected").
ErrTrustMarkDelegationWrongType = errors.New("federation: trust mark delegation header typ is not trust-mark-delegation+jwt")
Expand Down
18 changes: 9 additions & 9 deletions internal/federation/trustmark.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ type CreateTrustMarkParams struct {
Algorithm fapi.SignatureAlgorithm

// KeyID is recorded in the Trust Mark's "kid" header. Required —
// OpenID Federation 1.0 §7.1: "Trust Mark JWTs MUST include the kid
// OpenID Federation 1.0 §7: "Trust Mark JWTs MUST include the kid
// header parameter," unlike an Entity Statement's own KeyID
// (CreateParams' own field, optional there).
KeyID string
Expand Down Expand Up @@ -268,7 +268,7 @@ func CreateTrustMark(p CreateTrustMarkParams) (string, error) {
Issuer: p.Issuer, Subject: p.Subject, TrustMarkType: p.TrustMarkType,
Now: p.Now, Lifetime: p.Lifetime, Delegation: p.Delegation,
typ: trustMarkJWTType,
kidRequirement: `OpenID Federation 1.0 §7.1: "Trust Mark JWTs MUST include the kid header parameter"`,
kidRequirement: `OpenID Federation 1.0 §7: "Trust Mark JWTs MUST include the kid header parameter"`,
})
}

Expand Down Expand Up @@ -333,7 +333,7 @@ func createTrustMarkLikeJWT(p trustMarkLikeParams) (string, error) {
}

// trustMarkDelegationJWTType is the JWS "typ" header value every Trust
// Mark Delegation JWT MUST carry (OpenID Federation 1.0 §7.2: "MUST be
// Mark Delegation JWT MUST carry (OpenID Federation 1.0 §7.2.1: "MUST be
// explicitly typed, by setting the typ header parameter to
// trust-mark-delegation+jwt... Trust Mark delegation JWTs without a typ
// header parameter or with a different typ value MUST be rejected").
Expand All @@ -349,7 +349,7 @@ type TrustMarkDelegationClaims struct {
TrustMarkType string
IssuedAt time.Time

// ExpiresAt is zero if the "exp" claim was absent — per §7.2, "If
// ExpiresAt is zero if the "exp" claim was absent — per §7.2.1, "If
// not present, it means that the delegation does not expire."
ExpiresAt time.Time
}
Expand Down Expand Up @@ -412,19 +412,19 @@ type TrustMarkDelegationVerifyPolicy struct {
// ExpectedIssuer is the Trust Mark type's real owner, as named by
// the Trust Anchor's own "trust_mark_owners" claim — the
// delegation's iss claim must equal it exactly (OpenID Federation
// 1.0 §7.2's own validation step 5: "The Entity Identifier of the
// 1.0 §7.2.2's validation step 5: "The Entity Identifier of the
// Trust Mark Owner MUST match the value of iss in the delegation").
ExpectedIssuer string

// ExpectedSubject is the Trust Mark Issuer being delegated to — the
// delegation's sub claim must equal it exactly (§7.2's own step 4:
// delegation's sub claim must equal it exactly (§7.2.2 step 4:
// "The Entity Identifier of the Trust Mark Issuer MUST match the
// value of sub in the delegation").
ExpectedSubject string

// ExpectedTrustMarkType is the Trust Mark's own trust_mark_type —
// the delegation's trust_mark_type claim must equal it exactly
// (§7.2's own step 7).
// (§7.2.2 step 8).
ExpectedTrustMarkType string

// Algorithm is the algorithm the Trust Mark Owner is registered or
Expand Down Expand Up @@ -531,7 +531,7 @@ type CreateTrustMarkDelegationParams struct {
Algorithm fapi.SignatureAlgorithm

// KeyID is recorded in the delegation's "kid" header. Required —
// OpenID Federation 1.0 §7.2: "Trust Mark delegation JWTs MUST
// OpenID Federation 1.0 §7.2.1: "Trust Mark delegation JWTs MUST
// include the kid header parameter."
KeyID string

Expand Down Expand Up @@ -564,6 +564,6 @@ func CreateTrustMarkDelegation(p CreateTrustMarkDelegationParams) (string, error
Issuer: p.Issuer, Subject: p.Subject, TrustMarkType: p.TrustMarkType,
Now: p.Now, Lifetime: p.Lifetime,
typ: trustMarkDelegationJWTType,
kidRequirement: `OpenID Federation 1.0 §7.2: "Trust Mark delegation JWTs MUST include the kid header parameter"`,
kidRequirement: `OpenID Federation 1.0 §7.2.1: "Trust Mark delegation JWTs MUST include the kid header parameter"`,
})
}
2 changes: 1 addition & 1 deletion internal/jwe/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// content-encryption algorithm fapi.ContentEncryptionAlgorithm
// supports: A256GCM (a single AEAD primitive) or A256CBC-HS512
// (encrypt-then-MAC — AES-256-CBC plus a separate HMAC-SHA-512 tag,
// RFC 7518 §5.2.3).
// RFC 7518 §5.2.5).
//
// Like internal/jose, this package is intentionally low-level and
// generic: it knows nothing about ID tokens, nested JWTs, or any other
Expand Down
2 changes: 1 addition & 1 deletion internal/jwe/keywrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ var aesKeyWrapIV = [8]byte{0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6}
// aesKeyWrap wraps plaintext (which must be a multiple of 8 bytes, at
// least 16) under kek using AES Key Wrap (RFC 3394), returning
// ciphertext 8 bytes longer than plaintext. This exists because JOSE's
// ECDH-ES+A256KW key management (RFC 7518 §4.7) wraps the CEK this way,
// ECDH-ES+A256KW key management (RFC 7518 §4.6) wraps the CEK this way,
// and Go's standard library has no AES Key Wrap implementation — only
// the underlying AES block cipher this function builds on
// (crypto/aes.NewCipher).
Expand Down
2 changes: 1 addition & 1 deletion internal/requestobject/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type CreateParams struct {
// registered JWKS without trial and error.
KeyID string

// ClientID is the "iss" claim (RFC 9101 §5.3). If Parameters
// ClientID is the "iss" claim. If Parameters
// contains a "client_id" entry, it must equal ClientID exactly.
ClientID string

Expand Down
4 changes: 3 additions & 1 deletion internal/requestobject/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ var (
ErrMalformedClaims = errors.New("requestobject: malformed claims")

// ErrClientIDIssuerMismatch indicates the payload's "client_id"
// parameter (RFC 9101 §5.3) did not equal its "iss" claim.
// parameter did not equal its "iss" claim, which names the client
// that signed it (RFC 9101 §6.3 has the request's and the request
// object's client_id be identical).
ErrClientIDIssuerMismatch = errors.New("requestobject: client_id parameter does not match iss")

// ErrIssuerMismatch indicates the object's iss claim did not equal
Expand Down
10 changes: 6 additions & 4 deletions internal/token/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ var (
ErrNonceMismatch = errors.New("token: nonce does not match expected value")

// ErrMissingAuthorizedParty indicates an ID token's aud claim named
// more than one audience but carried no azp claim (OIDC Core
// §3.1.3.7 step 9) — needed to disambiguate which of those
// audiences the token was actually authorized for.
// more than one audience but carried no azp claim, needed to
// disambiguate which of those audiences the token was actually
// authorized for (see OIDC Core §2 on azp, and §3.1.3.7 steps 4-5).
// The current text of Core no longer has the client require it;
// this package still does.
ErrMissingAuthorizedParty = errors.New("token: aud has multiple audiences but azp is missing")

// ErrAuthorizedPartyMismatch indicates an ID token's azp claim did
// not equal the caller's own client ID (OIDC Core §3.1.3.7 step 10).
// not equal the caller's own client ID (OIDC Core §3.1.3.7 step 5).
ErrAuthorizedPartyMismatch = errors.New("token: azp does not match expected audience")

// ErrIssuedAtTooOld indicates an ID token's iat claim is further in
Expand Down
9 changes: 5 additions & 4 deletions internal/token/idtoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import (
// just the one-element case) — unlike internal/clientassertion's own
// Audience, which stays a single string because a client assertion is
// always addressed to exactly one token endpoint, never several
// audiences at once. §3.1.3.7 step 9 additionally says a client "SHOULD
// verify that an azp Claim is present" when there's more than one
// audience, and step 10 that it "SHOULD verify" azp equals the client's
// own ID when present — IDTokenValidatePolicy.Validate enforces both.
// audiences at once. IDTokenValidatePolicy.Validate requires an azp
// claim when there's more than one audience, which earlier editions of
// OIDC Core §3.1.3.7 had a client check and the current one leaves to
// it, and checks that azp, when present, equals the client's own ID
// (§3.1.3.7 step 5: the client "SHOULD validate the azp value").
//
// Everything beyond the claims this struct names is left in Parameters
// as raw JSON.
Expand Down
Loading
Loading