Help us shape the future of Go! Take the 2026 Go Developer Survey

dpop

package
v1.0.317 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 24, 2026 License: Apache-2.0 Imports: 28 Imported by: 4

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

Examples

Constants

View Source
const (
	// HTTPHeader header name for DPoP
	HTTPHeader = "DPoP"
	// ContentType value
	ContentType = "application/dpop+jwt"
)
View Source
const (
	// DefaultExpiration for the proof
	DefaultExpiration = time.Minute * 10
	// DefaultNotBefore offset for NotBefore
	DefaultNotBefore = -10 * time.Minute
)
View Source
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

View Source
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

View Source
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")
)
View Source
var TimeNowFn = time.Now

TimeNowFn to override in unit tests

Functions

func AccessTokenHash added in v0.29.306

func AccessTokenHash(accessToken string) string

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

func ForRequest(p Signer, r *http.Request, extraClaims any) (string, error)

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

func GetCnfClaim(claims map[string]any) (string, error)

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

func SetCnfClaim(claims map[string]any, thumbprint string)

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

func (c *MemoryReplayCache) Add(_ context.Context, key string, expiresAt time.Time) error

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

func NewSigner

func NewSigner(s crypto.Signer) (Signer, error)

NewSigner creates a DPoP signer that can generate DPoP headers for a request.

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

func GetTokenInfo(t string) *TokenInfo

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL