Documentation
¶
Overview ¶
Package dpop implements OAuth 2.0 Demonstrating Proof of Possession (RFC 9449): clients build DPoP proof JWTs for HTTP requests with Signer and ForRequest; servers verify them with VerifyRequestClaims or VerifyClaimsContext and bind the resulting key thumbprint to the access token "cnf.jkt" claim.
Verification is opt-in beyond the proof itself. Set VerifyConfig.ReplayCache (NewMemoryReplayCache for one process, or a shared store) to reject a replayed jti; without it replay detection is the caller's job. At a protected resource set both AccessToken, to require a matching "ath" claim, and ExpectedThumbprint, to the token "cnf.jkt"; these are per-request values, so set them on a copy of a shared config. Set ExternalURL when the server's public scheme or host differs from the request it receives:
cfg := dpop.VerifyConfig{
ExternalURL: "https://api.example.com",
ReplayCache: replayCache, // shared by all requests
AccessToken: accessToken,
ExpectedThumbprint: cnfJKT,
}
res, err := dpop.VerifyRequestClaims(cfg, req)
Clients add the "ath" claim with map[string]any{dpop.ClaimAccessTokenHash: dpop.AccessTokenHash(token)} as the ForRequest extra claims.
Index ¶
- Constants
- Variables
- func AccessTokenHash(accessToken string) string
- func ForRequest(p Signer, r *http.Request, extraClaims any) (string, error)
- func GenerateKey(label string) (*jose.JSONWebKey, error)
- func GetCnfClaim(claims map[string]any) (string, error)
- func LoadKey(path string) (*jose.JSONWebKey, string, error)
- func SaveKey(folder string, k *jose.JSONWebKey) (string, error)
- func SetCnfClaim(claims map[string]any, thumbprint string)
- func Thumbprint(k *jose.JSONWebKey) (string, error)
- type MemoryReplayCache
- type ReplayCache
- type Result
- type Signer
- type TokenInfo
- type VerifyConfig
Examples ¶
Constants ¶
const ( // HTTPHeader header name for DPoP HTTPHeader = "DPoP" // ContentType value ContentType = "application/dpop+jwt" )
const ( // DefaultExpiration for the proof DefaultExpiration = time.Minute * 10 // DefaultNotBefore offset for NotBefore DefaultNotBefore = -10 * time.Minute )
const ( // CnfThumbprint is the claim name for JKT thumbprint CnfThumbprint = "jkt" // ClaimAccessTokenHash is the proof claim name for the access token hash ClaimAccessTokenHash = "ath" )
https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop-04
const DefaultReplayCacheSize = 100000
DefaultReplayCacheSize is the capacity used by NewMemoryReplayCache when the requested size is not positive. Size the cache as peak accepted proofs per second times the acceptance window (DefaultExpiration plus jwt.DefaultTimeSkew).
Variables ¶
var ( // ErrReplay is returned when a proof with the same key and jti was // already accepted within its acceptance window. ErrReplay = errors.New("dpop: proof replayed") // ErrReplayCacheFull is returned by MemoryReplayCache when every entry // is still unexpired. The proof is rejected (fail closed). ErrReplayCacheFull = errors.New("dpop: replay cache is full") )
var TimeNowFn = time.Now
TimeNowFn to override in unit tests
Functions ¶
func AccessTokenHash ¶ added in v0.29.306
AccessTokenHash returns the ath claim value for accessToken: the base64url-encoded SHA-256 hash of its ASCII value (RFC 9449 §4.2). Clients add it to the proof extra claims when calling a protected resource.
func ForRequest ¶ added in v0.6.0
ForRequest creates a DPoP proof for the request method and URL and sets it as the DPoP header; the proof is also returned.
func GenerateKey ¶ added in v0.2.0
func GenerateKey(label string) (*jose.JSONWebKey, error)
GenerateKey returns JSONWebKey to sign JWT
func GetCnfClaim ¶ added in v0.2.0
GetCnfClaim gets DPoP `cnf` claim
func LoadKey ¶ added in v0.2.0
func LoadKey(path string) (*jose.JSONWebKey, string, error)
LoadKey returns *jose.JSONWebKey
func SaveKey ¶ added in v0.2.0
func SaveKey(folder string, k *jose.JSONWebKey) (string, error)
SaveKey saves the key to storage
func SetCnfClaim ¶ added in v0.2.0
SetCnfClaim sets DPoP `cnf` claim
func Thumbprint ¶ added in v0.2.0
func Thumbprint(k *jose.JSONWebKey) (string, error)
Thumbprint returns key thumbprint
Types ¶
type MemoryReplayCache ¶ added in v0.29.306
type MemoryReplayCache struct {
// contains filtered or unexported fields
}
MemoryReplayCache is a bounded, process-local ReplayCache. Expired entries are evicted before a new key is admitted; when the cache is still full the new proof is rejected with ErrReplayCacheFull. Failing closed means a client that can mint valid proofs (at a token endpoint, any client) can fill the cache and have other proofs rejected for up to the acceptance window; size it for peak load and rate-limit proof sources. It uses TimeNowFn as its clock.
func NewMemoryReplayCache ¶ added in v0.29.306
func NewMemoryReplayCache(maxEntries int) *MemoryReplayCache
NewMemoryReplayCache returns a MemoryReplayCache holding at most maxEntries unexpired proofs, or DefaultReplayCacheSize when maxEntries is not positive.
func (*MemoryReplayCache) Add ¶ added in v0.29.306
Add records key until expiresAt; see ReplayCache.
func (*MemoryReplayCache) Len ¶ added in v0.29.306
func (c *MemoryReplayCache) Len() int
Len returns the number of retained entries, including entries that have expired but were not evicted yet.
type ReplayCache ¶ added in v0.29.306
type ReplayCache interface {
// Add atomically records key through expiresAt (inclusive: the verifier
// still accepts a proof at that instant). It returns an error
// wrapping ErrReplay when key is already recorded and not expired, and
// any other error when the key cannot be recorded; VerifyClaims rejects
// the proof in both cases. Of concurrent Add calls for the same key,
// exactly one may succeed.
Add(ctx context.Context, key string, expiresAt time.Time) error
}
ReplayCache records accepted DPoP proofs so that VerifyClaims can reject a second use of the same proof (RFC 9449 §11.1). Implementations must be safe for concurrent use; a shared store (for example Redis SET NX with an expiry) is needed when several server instances accept proofs for the same resource.
type Result ¶
type Result struct {
Claims *jwtgo.Claims
Key *jose.JSONWebKey
// Thumbprint is the RFC 7638 SHA-256 thumbprint of Key, to compare
// with the access token cnf.jkt claim
Thumbprint string
// AccessTokenHash is the proof ath claim, if present
AccessTokenHash string
}
Result is returned from VerifyClaims
func VerifyClaims ¶
func VerifyClaims(cfg VerifyConfig, phdr, httpMethod, httpURI string) (*Result, error)
VerifyClaims is VerifyClaimsContext with context.Background().
func VerifyClaimsContext ¶ added in v0.29.306
func VerifyClaimsContext(ctx context.Context, cfg VerifyConfig, phdr, httpMethod, httpURI string) (*Result, error)
VerifyClaimsContext verifies DPoP proof phdr for a request with httpMethod and httpURI (RFC 9449 §4.3) and returns its claims, key and key thumbprint. The signature is verified with the embedded public jwk before any claim is used. htu is compared with httpURI after URI normalization (scheme and host case-insensitive, path case-sensitive, query and fragment ignored). When set, cfg.AccessToken requires a matching ath claim and must be paired with cfg.ExpectedThumbprint, which requires the proof key to match the access token cnf.jkt; cfg.ReplayCache records the proof after every other check passed, rejecting a replayed jti with ErrReplay. Without cfg.ReplayCache the caller is responsible for replay detection.
func VerifyRequestClaims ¶ added in v0.6.0
func VerifyRequestClaims(cfg VerifyConfig, req *http.Request) (*Result, error)
VerifyRequestClaims verifies the DPoP proof of an HTTP request received by a server; see VerifyClaims. The htu claim is compared with the request URI built from cfg.ExternalURL, or else from the request URL and Host (https when the URL has no scheme), without query and fragment. The request context is passed to cfg.ReplayCache.
Example ¶
A protected resource verifies the proof sent with a DPoP-bound access token: htu against its trusted external origin, ath against the presented token, the proof key against the token cnf.jkt, and jti against a replay cache shared by all requests.
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"net/http"
"net/http/httptest"
"github.com/effective-security/xpki/jwt/dpop"
)
func main() {
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
signer, _ := dpop.NewSigner(key)
accessToken := "access-token-bound-to-the-key"
cnfJKT := signer.JWKThumbprint() // from the access token cnf.jkt claim
// client
creq, _ := http.NewRequest(http.MethodGet, "https://api.example.com/v1/items?page=2", nil)
_, _ = dpop.ForRequest(signer, creq, map[string]any{
dpop.ClaimAccessTokenHash: dpop.AccessTokenHash(accessToken),
})
// server, behind a proxy that forwards the request as plain HTTP
replay := dpop.NewMemoryReplayCache(0)
cfg := dpop.VerifyConfig{
ExternalURL: "https://api.example.com",
ReplayCache: replay,
AccessToken: accessToken,
ExpectedThumbprint: cnfJKT,
}
sreq := httptest.NewRequest(http.MethodGet, "/v1/items?page=2", nil)
sreq.Host = "backend:8080"
sreq.Header.Set(dpop.HTTPHeader, creq.Header.Get(dpop.HTTPHeader))
res, err := dpop.VerifyRequestClaims(cfg, sreq)
fmt.Println(err, res.Claims.HTTPUri)
_, err = dpop.VerifyRequestClaims(cfg, sreq)
fmt.Println(err)
}
Output: <nil> https://api.example.com/v1/items dpop: proof rejected: dpop: proof replayed
type Signer ¶
type Signer interface {
// Sign returns DPoP token
Sign(ctx context.Context, method string, u *url.URL, extraClaims any) (string, error)
// JWKThumbprint returns base64 hash of the key
JWKThumbprint() string
}
Signer specifies an interface to sign HTTP requests with DPoP
type TokenInfo ¶ added in v0.2.0
type TokenInfo struct {
Token *jwt.JSONWebToken
Claims jwtgo.Claims
Key *jose.JSONWebKey
Thumbprint string
CnfJkt string
IsPublicKey bool
IsFresh bool
}
TokenInfo is returned from GetTokenInfo
func GetTokenInfo ¶ added in v0.2.0
GetTokenInfo returns token info, if it's JWT or nil otherwise
type VerifyConfig ¶
type VerifyConfig struct {
// ExpectedIssuer validates the iss claim of a JWT matches this value
ExpectedIssuer string
// ExpectedSubject validates the sub claim of a JWT matches this value
ExpectedSubject string
// ExpectedAudience validates that the aud claim of a JWT contains this value
ExpectedAudience string
// ExpectedNonce validates that the nonce claim of a JWT contains this value
ExpectedNonce string
// EnableQuery specifies to get `dpop` header from the QueryString
EnableQuery bool
// ExternalURL is the trusted external origin of this server, as
// scheme://host[:port]. When set, VerifyRequestClaims takes the htu
// scheme and host from it instead of the request URL and Host header,
// which are client-controlled. Set it for plain-HTTP servers and for
// servers reached under a different name than the Host they receive.
ExternalURL string
// ReplayCache, when set, records each accepted proof and rejects a
// second use of the same jti with the same key (ErrReplay) until the
// proof leaves its acceptance window. When nil, VerifyClaims does not
// detect replayed proofs.
ReplayCache ReplayCache
// AccessToken, when set, is the access token presented with the proof
// to a protected resource. The proof must then carry an ath claim equal
// to AccessTokenHash(AccessToken), and ExpectedThumbprint must be set
// too: ath alone does not bind the proof key to the token. Leave it
// empty at the token endpoint. Set it per request, on a copy of a shared
// VerifyConfig.
AccessToken string
// ExpectedThumbprint, when set, is the cnf.jkt value of the access token
// presented with the proof; the proof key thumbprint must equal it. Set
// it per request, with AccessToken.
ExpectedThumbprint string
}
VerifyConfig expresses the possible options for validating a JWT