Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
fed2e86
chore: initialize v3 module with updated dependencies
developerkunal Nov 21, 2025
ce89f57
feat: add framework-agnostic core package for v3
developerkunal Nov 21, 2025
ee4c73d
feat: refactor validator to use pure options pattern
developerkunal Nov 21, 2025
759182e
feat: add generic support for WithCustomClaims option
developerkunal Nov 21, 2025
f0cf67a
chore: update examples for generic WithCustomClaims and v3 module path
developerkunal Nov 21, 2025
7cf5d00
Merge branch 'v3-phase1-pr2-core-package' into v3-phase1-pr3-validato…
developerkunal Nov 21, 2025
78eee9e
refactor: migrate from go-jose v2 to jwx v3
developerkunal Nov 21, 2025
7245330
refactor(jwks,validator): implement pure options pattern and improve …
developerkunal Nov 21, 2025
4df0d15
test: add comprehensive tests for JWX algorithm conversion and valida…
developerkunal Nov 21, 2025
8df4068
test: skip known failing test and add unit test for jwk.Set handling …
developerkunal Nov 21, 2025
67f5425
test: add unit tests for CachingProvider configurations and error han…
developerkunal Nov 21, 2025
1da96e0
fix(tests): handle JSON encoder errors in mock server responses
developerkunal Nov 21, 2025
e8c1ad5
refactor: implement pure options pattern for middleware with core int…
developerkunal Nov 21, 2025
703d87d
Add Message for non-ErrNoCookie errors
developerkunal Nov 21, 2025
073a6b2
chore: remove unused dependencies from go.mod and go.sum
developerkunal Nov 21, 2025
ebac1df
docs: add documentation and linting configuration for v3
developerkunal Nov 24, 2025
b67ad41
fix: update golangci-lint config for v2.6.2 schema compliance
developerkunal Nov 24, 2025
d165192
refactor: use core context operations in HTTP middleware for consistency
developerkunal Nov 24, 2025
c7ca941
docs: add comments to JWTMiddleware for clarity on functionality and …
developerkunal Nov 24, 2025
e9ddaa4
Merge branch 'v3-phase1-pr5-middleware-options' into v3-phase1-pr6-do…
developerkunal Nov 24, 2025
54615e2
refactor: migrate middleware to accept validator instances
developerkunal Nov 25, 2025
23c5b61
Merge branch 'v3-phase1-pr5-middleware-options' into v3-phase1-pr6-do…
developerkunal Nov 25, 2025
c3e8206
docs: update all documentation to use WithValidator instead of WithVa…
developerkunal Nov 25, 2025
99842ab
Merge branch 'v3-dev' into v3-phase1-pr2-core-package
developerkunal Nov 27, 2025
abf302d
Merge branch 'v3-phase1-pr2-core-package' into v3-phase1-pr3-validato…
developerkunal Nov 27, 2025
54715a7
Merge branch 'v3-phase1-pr3-validator-options' into v3-phase1-pr4-jwx…
developerkunal Nov 27, 2025
9d0186a
Merge branch 'v3-phase1-pr4-jwx-migration' into v3-phase1-pr5-middlew…
developerkunal Nov 27, 2025
41c59e8
Merge branch 'v3-phase1-pr5-middleware-options' into v3-phase1-pr6-do…
developerkunal Nov 27, 2025
1805e5a
feat: add DPoP (Demonstrating Proof-of-Possession) support
developerkunal Nov 27, 2025
236bc4f
feat: enhance DPoP context tests and add edge case handling in middle…
developerkunal Nov 27, 2025
9b10ec5
Merge branch 'v3-phase1-pr6-documentation-linting' into v3-phase1-pr7…
developerkunal Nov 27, 2025
3950189
feat(extractor): return ExtractedToken with scheme from TokenExtractor
developerkunal Dec 2, 2025
a016db6
refactor(logging): streamline logging methods in token validation
developerkunal Dec 2, 2025
fac48f6
feat: add DPoP ATH mismatch error handling and tests for missing proo…
developerkunal Dec 2, 2025
b81246a
test(core): achieve 100% test coverage for core package
developerkunal Dec 3, 2025
2c1778e
Merge branch 'v3-dev' into v3-phase1-pr7-dpop
developerkunal Dec 9, 2025
f05fb72
fix(error-handler): implement RFC 6750 compliance for WWW-Authenticat…
developerkunal Dec 9, 2025
3529273
test: improve coverage for proxy, error handler, extractor, and options
developerkunal Dec 9, 2025
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
15 changes: 9 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ vendor/

# Docs
docs/
# Example binaries
examples/echo-example/echo
examples/gin-example/gin
examples/http-example/http
examples/http-jwks-example/http-jwks
examples/iris-example/iris

# Example binaries - ignore executables (not .go, .mod, .sum, .md files)
examples/*/echo
examples/*/gin
examples/*/iris
examples/*/http
examples/*/http-jwks
examples/*/http-dpop
examples/*/http-dpop-*
86 changes: 83 additions & 3 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,16 @@ This guide helps you migrate from go-jwt-middleware v2 to v3. While v3 introduce
| **Architecture** | Monolithic | Core-Adapter pattern |
| **Context Key** | `ContextKey{}` struct | Unexported `contextKey int` |
| **Type Names** | `ExclusionUrlHandler` | `ExclusionURLHandler` |
| **TokenExtractor** | Returns `string` | Returns `ExtractedToken` |
| **DPoP Support** | Not available | Full RFC 9449 support |

### Why Upgrade?

- βœ… **Better Performance**: lestrrat-go/jwx v3 is faster and more efficient
- βœ… **More Algorithms**: Support for EdDSA, ES256K, and all modern algorithms
- βœ… **Type Safety**: Generics eliminate type assertion errors at compile time
- βœ… **Better IDE Support**: Self-documenting options with autocomplete
- βœ… **Enhanced Security**: CVE mitigations and RFC 6750 compliance
- βœ… **Enhanced Security**: CVE mitigations, RFC 6750 compliance, and DPoP support
- βœ… **Modern Go**: Built for Go 1.23+ with modern patterns

## Breaking Changes
Expand Down Expand Up @@ -120,6 +122,26 @@ type ExclusionUrlHandler func(r *http.Request) bool
type ExclusionURLHandler func(r *http.Request) bool
```

### 5. TokenExtractor Signature Change

`TokenExtractor` now returns `ExtractedToken` (with scheme) instead of `string`:

**v2:**
```go
type TokenExtractor func(r *http.Request) (string, error)
```

**v3:**
```go
type ExtractedToken struct {
Token string
Scheme AuthScheme // AuthSchemeBearer, AuthSchemeDPoP, or AuthSchemeUnknown
}
type TokenExtractor func(r *http.Request) (ExtractedToken, error)
```

**Note:** Built-in extractors (`CookieTokenExtractor`, `ParameterTokenExtractor`, `MultiTokenExtractor`) work unchanged. Only custom extractors need updating.

## Step-by-Step Migration

### 1. Update Dependencies
Expand Down Expand Up @@ -328,15 +350,48 @@ if err != nil {

#### Token Extractors

No changes needed - same API:
**v3 Breaking Change**: `TokenExtractor` now returns `ExtractedToken` instead of `string`:

**v2:**
```go
// TokenExtractor returned string
type TokenExtractor func(r *http.Request) (string, error)
```

**v3:**
```go
// TokenExtractor returns ExtractedToken with both token and scheme
type ExtractedToken struct {
Token string
Scheme AuthScheme // bearer, dpop, or unknown
}
type TokenExtractor func(r *http.Request) (ExtractedToken, error)
```

Built-in extractors work the same way:
```go
// Both v2 and v3
// These all work unchanged - internal implementation updated
jwtmiddleware.CookieTokenExtractor("jwt")
jwtmiddleware.ParameterTokenExtractor("token")
jwtmiddleware.MultiTokenExtractor(extractors...)
```

**Custom extractors must be updated:**
```go
// v2
customExtractor := func(r *http.Request) (string, error) {
return r.Header.Get("X-Custom-Token"), nil
}

// v3
customExtractor := func(r *http.Request) (jwtmiddleware.ExtractedToken, error) {
return jwtmiddleware.ExtractedToken{
Token: r.Header.Get("X-Custom-Token"),
Scheme: jwtmiddleware.AuthSchemeUnknown, // or AuthSchemeBearer if you know
}, nil
}
```

### 5. Update Claims Access

#### Handler Claims Access
Expand Down Expand Up @@ -578,6 +633,31 @@ middleware, err := jwtmiddleware.New(
)
```

### 6. DPoP (Demonstrating Proof-of-Possession)

v3 adds full support for RFC 9449 DPoP, which provides proof-of-possession for access tokens:

```go
// DPoP modes:
// - DPoPAllowed (default): Accept both Bearer and DPoP tokens
// - DPoPRequired: Only accept DPoP tokens
// - DPoPDisabled: Ignore DPoP, reject DPoP scheme

middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithDPoPMode(jwtmiddleware.DPoPRequired),
)
```

DPoP validates:
- Proof signature using asymmetric algorithms (RS256, ES256, etc.)
- HTTP method and URL binding (`htm` and `htu` claims)
- Token binding via thumbprint (`jkt` claim in access token's `cnf`)
- Access token hash (`ath` claim) matching
- Replay protection via `jti` and `iat` claims

See the [DPoP examples](./examples/http-dpop-example) for complete working code.

## FAQ

### Q: Can I use v2 and v3 side by side during migration?
Expand Down
61 changes: 60 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@ jwtmiddleware.New(
### πŸ›‘οΈ Enhanced Security
- RFC 6750 compliant error responses
- Secure defaults (credentials required, clock skew = 0)
- **DPoP support** (RFC 9449) for proof-of-possession tokens

### πŸ”‘ DPoP (Demonstrating Proof-of-Possession)
Prevent token theft with proof-of-possession:

```go
jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithDPoPMode(jwtmiddleware.DPoPRequired),
)
```

## Getting Started

Expand Down Expand Up @@ -121,7 +132,7 @@ var handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
})

func main() {
keyFunc := func(ctx context.Context) (interface{}, error) {
keyFunc := func(ctx context.Context) (any, error) {
// Our token must be signed using this secret
return []byte("secret"), nil
}
Expand Down Expand Up @@ -442,12 +453,60 @@ jwtValidator, err := validator.New(
)
```

### DPoP (Demonstrating Proof-of-Possession)

v3 adds support for [DPoP (RFC 9449)](https://datatracker.ietf.org/doc/html/rfc9449), which provides proof-of-possession for access tokens. This prevents token theft and replay attacks.

#### DPoP Modes

| Mode | Description | Use Case |
|------|-------------|----------|
| **DPoPAllowed** (default) | Accepts both Bearer and DPoP tokens | Migration period, backward compatibility |
| **DPoPRequired** | Only accepts DPoP tokens | Maximum security |
| **DPoPDisabled** | Ignores DPoP proofs, rejects DPoP scheme | Legacy systems |

#### Basic DPoP Setup

```go
middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithDPoPMode(jwtmiddleware.DPoPAllowed), // Default
)
```

#### Require DPoP for Maximum Security

```go
middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithDPoPMode(jwtmiddleware.DPoPRequired),
)
```

#### Behind a Proxy

When running behind a reverse proxy, configure trusted proxy headers:

```go
middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithDPoPMode(jwtmiddleware.DPoPRequired),
jwtmiddleware.WithStandardProxy(), // Trust X-Forwarded-* headers
)
```

See the [DPoP examples](./examples/http-dpop-example) for complete working code.

## Examples

For complete working examples, check the [examples](./examples) directory:

- **[http-example](./examples/http-example)** - Basic HTTP server with HMAC
- **[http-jwks-example](./examples/http-jwks-example)** - Production setup with JWKS and Auth0
- **[http-dpop-example](./examples/http-dpop-example)** - DPoP support (allowed mode)
- **[http-dpop-required](./examples/http-dpop-required)** - DPoP required mode
- **[http-dpop-disabled](./examples/http-dpop-disabled)** - DPoP disabled mode
- **[http-dpop-trusted-proxy](./examples/http-dpop-trusted-proxy)** - DPoP behind reverse proxy
- **[gin-example](./examples/gin-example)** - Integration with Gin framework
- **[echo-example](./examples/echo-example)** - Integration with Echo framework
- **[iris-example](./examples/iris-example)** - Integration with Iris framework
Expand Down
106 changes: 106 additions & 0 deletions core/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ type contextKey int

const (
claimsKey contextKey = iota
dpopContextKey
authSchemeKey
dpopModeKey
)

// GetClaims retrieves claims from the context with type safety using generics.
Expand Down Expand Up @@ -53,3 +56,106 @@ func SetClaims(ctx context.Context, claims any) context.Context {
func HasClaims(ctx context.Context) bool {
return ctx.Value(claimsKey) != nil
}

// SetDPoPContext stores DPoP context in the context.
// This is a helper function for adapters to set DPoP context after validation.
//
// DPoP context contains information about the validated DPoP proof, including
// the public key thumbprint, issued-at timestamp, and the raw proof JWT.
func SetDPoPContext(ctx context.Context, dpopCtx *DPoPContext) context.Context {
return context.WithValue(ctx, dpopContextKey, dpopCtx)
}

// GetDPoPContext retrieves DPoP context from the context.
// Returns nil if no DPoP context exists (e.g., for Bearer tokens).
//
// Example usage:
//
// dpopCtx := core.GetDPoPContext(ctx)
// if dpopCtx != nil {
// log.Printf("DPoP token from key: %s", dpopCtx.PublicKeyThumbprint)
// }
func GetDPoPContext(ctx context.Context) *DPoPContext {
val := ctx.Value(dpopContextKey)
if val == nil {
return nil
}

dpopCtx, ok := val.(*DPoPContext)
if !ok {
return nil
}

return dpopCtx
}

// HasDPoPContext checks if a DPoP context exists in the context.
// Returns true for DPoP-bound tokens, false for Bearer tokens.
//
// Example usage:
//
// if core.HasDPoPContext(ctx) {
// dpopCtx := core.GetDPoPContext(ctx)
// // Handle DPoP-specific logic...
// }
func HasDPoPContext(ctx context.Context) bool {
return ctx.Value(dpopContextKey) != nil
}

// SetAuthScheme stores the authorization scheme in the context.
// This is used by adapters to track which auth scheme was used in the request.
func SetAuthScheme(ctx context.Context, scheme AuthScheme) context.Context {
return context.WithValue(ctx, authSchemeKey, scheme)
}

// GetAuthScheme retrieves the authorization scheme from the context.
// Returns AuthSchemeUnknown if no scheme was set.
//
// Example usage:
//
// scheme := core.GetAuthScheme(ctx)
// if scheme == core.AuthSchemeDPoP {
// // Handle DPoP-specific logic...
// }
func GetAuthScheme(ctx context.Context) AuthScheme {
val := ctx.Value(authSchemeKey)
if val == nil {
return AuthSchemeUnknown
}

scheme, ok := val.(AuthScheme)
if !ok {
return AuthSchemeUnknown
}

return scheme
}

// SetDPoPMode stores the DPoP mode in the context.
// This is used by adapters to track the DPoP mode configuration for error handling.
func SetDPoPMode(ctx context.Context, mode DPoPMode) context.Context {
return context.WithValue(ctx, dpopModeKey, mode)
}

// GetDPoPMode retrieves the DPoP mode from the context.
// Returns DPoPAllowed if no mode was set (default).
//
// Example usage:
//
// mode := core.GetDPoPMode(ctx)
// if mode == core.DPoPRequired {
// // Only accept DPoP tokens
// }
func GetDPoPMode(ctx context.Context) DPoPMode {
val := ctx.Value(dpopModeKey)
if val == nil {
return DPoPAllowed // Default mode
}

mode, ok := val.(DPoPMode)
if !ok {
return DPoPAllowed
}

return mode
}
Loading
Loading