Skip to content

Commit 1805e5a

Browse files
feat: add DPoP (Demonstrating Proof-of-Possession) support
Implements RFC 9449 DPoP support for sender-constrained OAuth 2.0 tokens. Key Features: - Unified Validator interface supporting both JWT and DPoP validation - Three DPoP modes: Disabled, DPoPIfPresent (default), DPoPRequired - Automatic DPoP/Bearer token scheme detection - DPoP proof validation (HTM, HTU, JKT claims) - Trusted proxy support for URL reconstruction - Configurable proof age offset and IAT leeway Core Changes: - Added CheckTokenWithDPoP method to core.Core - Implemented DPoP context for accessing proof claims - Added DPoP-specific error codes and handling Validator: - Added ValidateDPoPProof method - JWK thumbprint computation and verification - dpop+jwt type validation Middleware: - WithDPoPMode, WithDPoPProofOffset, WithDPoPIATLeeway options - WithDPoPHeaderExtractor for custom header extraction - WithTrustedProxies for reverse proxy deployments Examples: - http-dpop-example: Full DPoP with Bearer fallback - http-dpop-required: Strict DPoP enforcement - http-dpop-disabled: Explicit opt-out - http-dpop-trusted-proxy: Production behind proxies Tests: 70+ new tests, 95%+ coverage maintained
1 parent c3e8206 commit 1805e5a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+8002
-125
lines changed

.gitignore

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ vendor/
1717

1818
# Docs
1919
docs/
20-
# Example binaries
21-
examples/echo-example/echo
22-
examples/gin-example/gin
23-
examples/http-example/http
24-
examples/http-jwks-example/http-jwks
25-
examples/iris-example/iris
20+
21+
# Example binaries - ignore executables (not .go, .mod, .sum, .md files)
22+
examples/*/echo
23+
examples/*/gin
24+
examples/*/iris
25+
examples/*/http
26+
examples/*/http-jwks
27+
examples/*/http-dpop
28+
examples/*/http-dpop-*

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ var handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
121121
})
122122

123123
func main() {
124-
keyFunc := func(ctx context.Context) (interface{}, error) {
124+
keyFunc := func(ctx context.Context) (any, error) {
125125
// Our token must be signed using this secret
126126
return []byte("secret"), nil
127127
}

core/context.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type contextKey int
99

1010
const (
1111
claimsKey contextKey = iota
12+
dpopContextKey
1213
)
1314

1415
// GetClaims retrieves claims from the context with type safety using generics.
@@ -53,3 +54,48 @@ func SetClaims(ctx context.Context, claims any) context.Context {
5354
func HasClaims(ctx context.Context) bool {
5455
return ctx.Value(claimsKey) != nil
5556
}
57+
58+
// SetDPoPContext stores DPoP context in the context.
59+
// This is a helper function for adapters to set DPoP context after validation.
60+
//
61+
// DPoP context contains information about the validated DPoP proof, including
62+
// the public key thumbprint, issued-at timestamp, and the raw proof JWT.
63+
func SetDPoPContext(ctx context.Context, dpopCtx *DPoPContext) context.Context {
64+
return context.WithValue(ctx, dpopContextKey, dpopCtx)
65+
}
66+
67+
// GetDPoPContext retrieves DPoP context from the context.
68+
// Returns nil if no DPoP context exists (e.g., for Bearer tokens).
69+
//
70+
// Example usage:
71+
//
72+
// dpopCtx := core.GetDPoPContext(ctx)
73+
// if dpopCtx != nil {
74+
// log.Printf("DPoP token from key: %s", dpopCtx.PublicKeyThumbprint)
75+
// }
76+
func GetDPoPContext(ctx context.Context) *DPoPContext {
77+
val := ctx.Value(dpopContextKey)
78+
if val == nil {
79+
return nil
80+
}
81+
82+
dpopCtx, ok := val.(*DPoPContext)
83+
if !ok {
84+
return nil
85+
}
86+
87+
return dpopCtx
88+
}
89+
90+
// HasDPoPContext checks if a DPoP context exists in the context.
91+
// Returns true for DPoP-bound tokens, false for Bearer tokens.
92+
//
93+
// Example usage:
94+
//
95+
// if core.HasDPoPContext(ctx) {
96+
// dpopCtx := core.GetDPoPContext(ctx)
97+
// // Handle DPoP-specific logic...
98+
// }
99+
func HasDPoPContext(ctx context.Context) bool {
100+
return ctx.Value(dpopContextKey) != nil
101+
}

core/core.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import (
1010
"time"
1111
)
1212

13-
// TokenValidator defines the interface for JWT validation.
14-
// Implementations should validate the token and return the validated claims.
15-
type TokenValidator interface {
13+
// Validator defines the interface for JWT and DPoP validation.
14+
// Implementations should validate tokens and DPoP proofs, returning the validated claims.
15+
type Validator interface {
1616
ValidateToken(ctx context.Context, token string) (any, error)
17+
ValidateDPoPProof(ctx context.Context, proofString string) (DPoPProofClaims, error)
1718
}
1819

1920
// Logger defines an optional logging interface for the core middleware.
@@ -28,9 +29,14 @@ type Logger interface {
2829
// It contains the core logic for token validation without any dependency
2930
// on specific transport protocols (HTTP, gRPC, etc.).
3031
type Core struct {
31-
validator TokenValidator
32+
validator Validator
3233
credentialsOptional bool
3334
logger Logger
35+
36+
// DPoP fields
37+
dpopMode DPoPMode
38+
dpopProofOffset time.Duration
39+
dpopIATLeeway time.Duration
3440
}
3541

3642
// CheckToken validates a JWT token string and returns the validated claims.

core/core_test.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import (
99
"github.com/stretchr/testify/require"
1010
)
1111

12-
// mockValidator is a mock implementation of TokenValidator for testing.
12+
// mockValidator is a mock implementation of Validator for testing.
1313
type mockValidator struct {
14-
validateFunc func(ctx context.Context, token string) (any, error)
14+
validateFunc func(ctx context.Context, token string) (any, error)
15+
dpopValidateFunc func(ctx context.Context, proof string) (DPoPProofClaims, error)
1516
}
1617

1718
func (m *mockValidator) ValidateToken(ctx context.Context, token string) (any, error) {
@@ -21,6 +22,13 @@ func (m *mockValidator) ValidateToken(ctx context.Context, token string) (any, e
2122
return nil, errors.New("not implemented")
2223
}
2324

25+
func (m *mockValidator) ValidateDPoPProof(ctx context.Context, proof string) (DPoPProofClaims, error) {
26+
if m.dpopValidateFunc != nil {
27+
return m.dpopValidateFunc(ctx, proof)
28+
}
29+
return nil, errors.New("not implemented")
30+
}
31+
2432
// mockLogger is a mock implementation of Logger for testing.
2533
type mockLogger struct {
2634
debugCalls []logCall

0 commit comments

Comments
 (0)