Documentation
¶
Overview ¶
Package dpop implements Demonstrating Proof of Possession (DPoP) for OAuth 2.0 per RFC 9449. DPoP binds access and refresh tokens to a client's ephemeral key pair, preventing bearer-token theft from being useful without the corresponding private key.
Flow:
- Client generates an ephemeral EC/RSA key pair.
- On every token request, the client creates a DPoP proof JWT signed by the private key and sends it in the DPoP header.
- This server validates the proof, extracts the public key thumbprint (JWK SHA-256 thumbprint, RFC 7638), and embeds it as cnf.jkt in the issued access token.
- On every resource request the client sends a fresh DPoP proof. Resource servers verify both the access token cnf.jkt and the proof signature.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrInvalidNonce = errors.New("dpop: invalid or expired nonce")
ErrInvalidNonce is returned by ConsumeNonce when the presented nonce is unknown, already used, or expired.
Functions ¶
func AccessTokenHash ¶
AccessTokenHash computes the base64url-encoded SHA-256 hash of a raw access token string, as required by the DPoP ath claim (RFC 9449 §4.2).
func ExtractProofNonce ¶
ExtractProofNonce reads the `nonce` claim from a DPoP proof JWT without verifying its signature. The nonce's security derives from being server-issued and single-use (validated against NonceStore), so reading it unverified here is safe — the proof signature is validated separately. Returns "" when the proof is absent or has no nonce claim.
func Middleware ¶
func Middleware(store JTIStore, getRequestURL func(r *http.Request) string) func(http.Handler) http.Handler
Middleware returns an HTTP middleware that optionally validates a DPoP proof when present. When the DPoP header is absent the request is treated as a standard Bearer request. When the header is present but invalid, the request is rejected with 401.
The validated *Claims are stored in the request context under the key exported by ClaimsFromContext.
Types ¶
type Claims ¶
type Claims struct {
// JTI is the unique identifier of this proof (replay prevention).
JTI string
// HTTPMethod is the HTTP method the proof was created for.
HTTPMethod string
// HTTPURL is the full HTTP URL the proof was created for.
HTTPURL string
// IssuedAt is when the proof was signed.
IssuedAt time.Time
// Thumbprint is the SHA-256 JWK thumbprint of the proof's public key.
Thumbprint string
}
Claims holds the validated contents of a DPoP proof JWT.
func ClaimsFromContext ¶
ClaimsFromContext retrieves the DPoP *Claims stored by Middleware. Returns nil when the request did not carry a DPoP proof.
func ValidateProof ¶
func ValidateProof( ctx context.Context, proofHeader string, method string, requestURL string, accessTokenHash string, store JTIStore, ) (*Claims, error)
ValidateProof validates a DPoP proof JWT and returns the extracted claims.
Parameters:
- proofHeader: the raw value of the DPoP HTTP header.
- method: expected HTTP method (e.g. "POST").
- requestURL: expected full request URL (e.g. "https://auth.example.com/oauth/token").
- accessTokenHash: SHA-256 hash of the access token encoded as base64url (ath claim). Pass "" to skip ath validation (during token issuance there is no access token yet).
- store: JTI denylist for replay prevention. May be nil (disables replay prevention — only safe in tests).
func ValidateResourceRequest ¶
func ValidateResourceRequest( ctx context.Context, proofHeader string, method string, requestURL string, accessToken string, cnfJKT string, store JTIStore, ) (*Claims, error)
ValidateResourceRequest performs full DPoP validation for a resource endpoint. It validates the proof and additionally verifies the proof's JWK thumbprint matches the access token's cnf.jkt claim (RFC 9449 §7).
type JTIStore ¶
type JTIStore interface {
DenyJTI(ctx context.Context, jti string, ttl time.Duration) error
IsJTIDenied(ctx context.Context, jti string) (bool, error)
}
JTIStore is the interface used to check and record DPoP proof JTIs for replay prevention. Implemented by the Redis cache.
type NonceManager ¶
type NonceManager struct {
// contains filtered or unexported fields
}
func NewNonceManager ¶
func NewNonceManager() *NonceManager
func (*NonceManager) Generate ¶
func (nm *NonceManager) Generate() string
func (*NonceManager) SetNonceHeader ¶
func (nm *NonceManager) SetNonceHeader(w http.ResponseWriter)
func (*NonceManager) Stop ¶
func (nm *NonceManager) Stop()
func (*NonceManager) Validate ¶
func (nm *NonceManager) Validate(nonce string) bool
type NonceStore ¶
type NonceStore interface {
// SaveNonce persists a freshly issued nonce for a client with a TTL.
SaveNonce(tenantID, clientID int64, nonce string, expiresAt time.Time) error
// ConsumeNonce atomically validates and marks a nonce used. Returns ok=false
// when the nonce is unknown, already used, or expired.
ConsumeNonce(nonce string) (ok bool, err error)
}
NonceStore persists server-issued, single-use DPoP nonces. It is implemented by the oauth_dpop_nonces repository and injected at the composition root, so this platform package stays domain-agnostic. The method signatures match the repository exactly, so the repository satisfies this interface directly.
type StoreNonceManager ¶
type StoreNonceManager struct {
// contains filtered or unexported fields
}
StoreNonceManager issues and consumes DB-backed, single-use, per-client DPoP server nonces (RFC 9449 §8), unlike the in-memory NonceManager. It is used by the token endpoint's nonce gate for DPoP-required clients.
func NewStoreNonceManager ¶
func NewStoreNonceManager(store NonceStore) *StoreNonceManager
NewStoreNonceManager creates a store-backed nonce manager.
func (*StoreNonceManager) ConsumeNonce ¶
func (m *StoreNonceManager) ConsumeNonce(_ context.Context, nonce string) error
ConsumeNonce validates and single-use-consumes a nonce. Returns ErrInvalidNonce when the nonce is unknown, already used, or expired.
func (*StoreNonceManager) IssueNonce ¶
IssueNonce generates a fresh 32-byte base64url nonce, persists it for the client with a 5-minute TTL, and returns it for the DPoP-Nonce response header.