Skip to content

Conversation

jhrozek
Copy link
Contributor

@jhrozek jhrozek commented Oct 7, 2025

Implements HTTP middleware that automatically exchanges downstream authentication tokens for backend-specific tokens using RFC 8693 OAuth 2.0 Token Exchange.

The middleware extracts subject tokens from authenticated requests and replaces them with exchanged tokens, supporting two injection strategies: replacing the Authorization header or adding a custom header while preserving the original.

Fixes: #2065

@jhrozek jhrozek requested review from JAORMX, blkt and yrobla October 7, 2025 19:34
@jhrozek
Copy link
Contributor Author

jhrozek commented Oct 7, 2025

@claude review with focus on security and with taking the patterns other middlewares, especially the auth one taks

@jhrozek jhrozek requested a review from Copilot October 7, 2025 19:34
Copy link
Contributor

claude bot commented Oct 7, 2025

Claude finished @jhrozek's task —— View job


Code Review - Security & Middleware Patterns

I've completed a thorough review of the token exchange middleware implementation focusing on security concerns and consistency with existing middleware patterns.


✅ Strengths

Excellent security practices:

  1. Token redaction in logging - All sensitive values (tokens, secrets) are properly redacted in String() methods (exchange.go:100-159)
  2. Response size limits - Proper DoS prevention with maxResponseBodySize constant (exchange.go:34)
  3. No credential leakage - Comprehensive tests verify secrets never appear in error messages (exchange_test.go:603-664)
  4. Proper URL encoding - Client credentials are correctly URL-encoded per RFC 6749 (exchange.go:402)
  5. Context propagation - Request contexts are properly passed through the middleware chain

Strong middleware pattern consistency:

  • Factory pattern matches auth middleware (CreateMiddleware function)
  • Proper Close() method implementation
  • Handler() returns MiddlewareFunction correctly
  • JSON parameter marshaling follows same pattern as auth middleware

Comprehensive test coverage:

  • 593 lines of middleware tests covering success/failure/edge cases
  • Tests for both header injection strategies
  • Proper use of parallel testing (t.Parallel())

🔒 Security Concerns

1. Missing Token Source Validation (High Priority)

Location: pkg/auth/tokenexchange/middleware.go:201

The middleware doesn't validate that the subject token actually came from the auth middleware. An attacker could potentially bypass authentication by directly sending a request with a Bearer token to an endpoint configured with token exchange but without auth middleware.

Current flow:

// Line 155: Gets claims from context
claims, ok := r.Context().Value(auth.ClaimsContextKey{}).(jwt.MapClaims)
if !ok {
    logger.Debug("No claims found in context, proceeding without token exchange")
    next.ServeHTTP(w, r)  // ⚠️ Proceeds even without claims
    return
}

// Line 162: Extracts token from Authorization header
authHeader := r.Header.Get("Authorization")

Problem: If there are no claims (meaning auth middleware didn't run or failed), the middleware still proceeds to the next handler. This creates a potential security gap if token exchange middleware is accidentally configured without auth middleware.

Recommendation:

  1. Add a configuration option to require claims (fail closed by default):
type Config struct {
    // ... existing fields ...
    
    // RequireClaims determines behavior when claims are not found
    // If true (recommended default), returns 401 when claims are missing
    // If false, proceeds without token exchange (current behavior)
    RequireClaims bool `json:"require_claims,omitempty"`
}
  1. Update the middleware logic:
claims, ok := r.Context().Value(auth.ClaimsContextKey{}).(jwt.MapClaims)
if !ok {
    if config.RequireClaims {
        logger.Warn("Token exchange middleware requires authentication claims but none found")
        http.Error(w, "Authentication required", http.StatusUnauthorized)
        return
    }
    logger.Debug("No claims found in context, proceeding without token exchange")
    next.ServeHTTP(w, r)
    return
}
  1. Document in CLAUDE.md or middleware docs that token exchange middleware MUST be placed after auth middleware in the chain.

2. Token Correlation Gap (Medium Priority)

Location: pkg/auth/tokenexchange/middleware.go:162-175

The middleware extracts the token from the Authorization header separately from getting claims from context. There's no verification that the token in the header matches the token that was validated by the auth middleware.

Scenario: If an attacker can manipulate the Authorization header after auth validation but before token exchange (unlikely but worth considering in complex proxy scenarios), they could potentially exchange a different token than the one that was validated.

Comparison with auth middleware: The auth middleware at pkg/auth/token.go:789-822 extracts and validates the token atomically - it gets the token from the header, validates it, and stores claims in the context all in one operation.

Recommendation:
Consider one of these approaches:

Option A: Store the original token in context during auth middleware:

// In auth middleware (token.go):
type TokenContextKey struct{}
ctx = context.WithValue(ctx, TokenContextKey{}, tokenString)
ctx = context.WithValue(ctx, ClaimsContextKey{}, claims)

// In tokenexchange middleware:
subjectToken, ok := r.Context().Value(auth.TokenContextKey{}).(string)
if !ok || subjectToken == "" {
    // Token must come from context, not header
    http.Error(w, "Invalid authentication state", http.StatusInternalServerError)
    return
}

Option B: Add a hash/signature to claims to verify token hasn't changed:

// In auth middleware, add token hash to claims:
tokenHash := sha256.Sum256([]byte(tokenString))
claims["_token_hash"] = hex.EncodeToString(tokenHash[:])

// In tokenexchange middleware, verify:
headerToken := strings.TrimPrefix(authHeader, "Bearer ")
tokenHash := sha256.Sum256([]byte(headerToken))
if claims["_token_hash"] != hex.EncodeToString(tokenHash[:]) {
    http.Error(w, "Token mismatch", http.StatusUnauthorized)
    return
}

Option A is cleaner and more secure.

3. Custom Header Name Validation (Low Priority)

Location: pkg/auth/tokenexchange/middleware.go:107-108

The custom header name is only validated for emptiness, not for security issues:

if config.HeaderStrategy == HeaderStrategyCustom && config.ExternalTokenHeaderName == "" {
    return fmt.Errorf("external_token_header_name must be specified...")
}

Concern: Malicious or misconfigured header names could:

  • Overwrite critical headers (e.g., "Host", "Content-Type")
  • Use non-standard characters that could cause issues
  • Be too long (potential DoS vector)

Recommendation:

func validateHeaderName(name string) error {
    // Check length
    if len(name) > 256 {
        return fmt.Errorf("header name too long (max 256 characters)")
    }
    
    // Check for protected headers (case-insensitive)
    protected := []string{"Host", "Content-Type", "Content-Length", "Authorization"}
    for _, p := range protected {
        if strings.EqualFold(name, p) {
            return fmt.Errorf("cannot use protected header: %s", name)
        }
    }
    
    // Check for valid header name characters (RFC 7230)
    for _, c := range name {
        if !isValidHeaderChar(c) {
            return fmt.Errorf("invalid character in header name")
        }
    }
    
    return nil
}

📋 Pattern Consistency Issues

4. Error Response Pattern Differs from Auth Middleware

Location: pkg/auth/tokenexchange/middleware.go:205-206, 211-213

Token exchange middleware:

if err != nil {
    logger.Warnf("Token exchange failed: %v", err)
    http.Error(w, "Token exchange failed", http.StatusUnauthorized)
    return
}

Auth middleware pattern (pkg/auth/token.go:813-815):

if err != nil {
    w.Header().Set("WWW-Authenticate", v.buildWWWAuthenticate(true, err.Error()))
    http.Error(w, fmt.Sprintf("Invalid token: %v", err), http.StatusUnauthorized)
    return
}

Observations:

  1. Auth middleware sets WWW-Authenticate header per RFC 6750/9728
  2. Auth middleware includes error details in response body
  3. Token exchange middleware uses generic "Token exchange failed" message

Recommendation:
While token exchange errors should probably remain generic for security (don't reveal backend exchange failures), consider:

  1. Adding WWW-Authenticate header for consistency with OAuth2 patterns
  2. Using http.StatusForbidden (403) instead of StatusUnauthorized (401) for token exchange failures since the user IS authenticated but the exchange failed
  3. Document this design decision
if err != nil {
    logger.Warnf("Token exchange failed for subject %v: %v", sub, err)
    // Use 403 instead of 401 - user is authenticated but exchange failed
    http.Error(w, "Token exchange failed", http.StatusForbidden)
    return
}

5. Missing Context Timeout

Location: pkg/auth/tokenexchange/middleware.go:201

The token exchange operation could potentially hang indefinitely if the OAuth server is slow/unresponsive.

Auth middleware comparison: The auth middleware has timeout handling in token validator (token.go:557) with a 5-second timeout for JWKS registration.

Token exchange: Uses the default HTTP client timeout (30 seconds - exchange.go:31) but doesn't have per-request timeout control.

Recommendation:

// Create context with timeout for token exchange
exchangeCtx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()

// Get token from token source with timeout context
tokenSource := exchangeConfig.TokenSource(exchangeCtx)
exchangedToken, err := tokenSource.Token()

🔍 Minor Issues

6. Inconsistent Logger Usage

Location: pkg/auth/tokenexchange/middleware.go:178-180

if sub, exists := claims["sub"]; exists {
    logger.Debugf("Performing token exchange for subject: %v", sub)
}

Issue: This logs the subject identifier, which could be considered PII in some contexts. The auth middleware is more careful about what it logs.

Recommendation: Consider making this configurable or use a hash:

if sub, exists := claims["sub"]; exists {
    // Hash the subject for logging to avoid PII concerns
    subHash := sha256.Sum256([]byte(fmt.Sprint(sub)))
    logger.Debugf("Performing token exchange for subject hash: %x", subHash[:8])
}

7. Test Coverage for Concurrent Requests

The test suite is excellent but doesn't include tests for concurrent middleware execution. Token exchange could be CPU/memory intensive under load.

Recommendation: Add a test:

func TestCreateTokenExchangeMiddlewareFromClaims_Concurrent(t *testing.T) {
    // Test that middleware handles concurrent requests safely
    // Verify no race conditions in token exchange
}

8. Missing Integration Test with Auth Middleware

There are no tests that verify the token exchange middleware works correctly when chained after the auth middleware.

Recommendation: Add an integration test in middleware_test.go:

func TestTokenExchangeWithAuthMiddleware_Integration(t *testing.T) {
    // Test: auth middleware -> token exchange middleware -> handler
    // Verify claims flow correctly through the chain
    // Verify token is properly exchanged
}

✨ Positive Observations

  1. RFC 8693 Compliance - Excellent implementation of OAuth 2.0 Token Exchange spec
  2. Test Quality - Very comprehensive test suite with good edge case coverage
  3. Error Handling - Proper error wrapping with context
  4. Documentation - Good inline comments explaining RFC compliance
  5. Code Organization - Clear separation of concerns (exchange logic vs middleware)

📝 Summary & Recommendations Priority

Must Fix (Security):

  1. Add RequireClaims config option and fail closed by default (fix(typo): corrects readme #1)
  2. Implement token correlation check to prevent header manipulation (Do we want the container monitor? #2)

Should Fix (Security & Consistency):
3. Add custom header name validation (#3)
4. Change error response code from 401 to 403 for exchange failures (#4)
5. Add context timeout for token exchange operations (#5)

Nice to Have:
6. Hash subject identifier in logs to avoid PII concerns (#6)
7. Add concurrent execution test (#7)
8. Add integration test with auth middleware (#8)


Additional Notes

The code is well-written and follows good security practices overall. The main concerns are around the interaction between auth and token exchange middlewares, which should be tightened to prevent misconfiguration vulnerabilities. The pattern consistency with auth middleware is generally good, though error handling could be more aligned.

Great work on the comprehensive test coverage and RFC compliance!


Job Run | Branch: thv-te-mware

Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This pull request adds HTTP middleware for OAuth 2.0 token exchange (RFC 8693) that automatically replaces downstream authentication tokens with backend-specific tokens. The middleware intercepts authenticated requests and exchanges their tokens using configurable injection strategies.

Key changes:

  • Implements token exchange middleware with configurable header injection strategies
  • Adds comprehensive test coverage for configuration validation, token injection, and error handling
  • Updates existing code to rename Config to ExchangeConfig to avoid naming conflicts

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
pkg/auth/tokenexchange/middleware.go Core middleware implementation with configuration validation and token injection logic
pkg/auth/tokenexchange/middleware_test.go Comprehensive test suite covering all middleware functionality and edge cases
pkg/auth/tokenexchange/exchange.go Renames Config struct to ExchangeConfig to avoid conflicts with middleware config
pkg/auth/tokenexchange/exchange_test.go Updates test references to use renamed ExchangeConfig struct

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@jhrozek
Copy link
Contributor Author

jhrozek commented Oct 7, 2025

I will address the comments tomorrow, but I like the suggestions about making sure the token came through the auth middleware and throwing a 403. The rest seems bogus or not needed.

Copy link
Contributor

@yrobla yrobla left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added some comments

dmjb
dmjb previously approved these changes Oct 8, 2025
Implements HTTP middleware that automatically exchanges downstream
authentication tokens for backend-specific tokens using RFC 8693
OAuth 2.0 Token Exchange.

The middleware extracts subject tokens from authenticated requests
and replaces them with exchanged tokens, supporting two injection
strategies: replacing the Authorization header or adding a custom
header while preserving the original.

Fixes: #2065
Copy link
Contributor

@blkt blkt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few minor comments, looks good so far.

Copy link

codecov bot commented Oct 8, 2025

Codecov Report

❌ Patch coverage is 95.90164% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 48.87%. Comparing base (2d6a56f) to head (98c4962).
⚠️ Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
pkg/auth/tokenexchange/middleware.go 95.83% 4 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2113      +/-   ##
==========================================
+ Coverage   48.71%   48.87%   +0.16%     
==========================================
  Files         241      242       +1     
  Lines       30592    30714     +122     
==========================================
+ Hits        14902    15013     +111     
- Misses      14567    14575       +8     
- Partials     1123     1126       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

blkt
blkt previously approved these changes Oct 8, 2025
Copy link
Contributor

@blkt blkt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor quality-of-life improvement, feel free to disregard.

@jhrozek
Copy link
Contributor Author

jhrozek commented Oct 8, 2025

Minor quality-of-life improvement, feel free to disregard.

I will post them as separate patches, but will keep moving with this PR if you don't mind.

@jhrozek
Copy link
Contributor Author

jhrozek commented Oct 8, 2025

Minor quality-of-life improvement, feel free to disregard.

I will post them as separate patches, but will keep moving with this PR if you don't mind.

Actually no you're right @blkt I took the suggestion.

Copy link
Contributor

@blkt blkt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@jhrozek jhrozek merged commit 6770e84 into main Oct 9, 2025
45 of 46 checks passed
@jhrozek jhrozek deleted the thv-te-mware branch October 9, 2025 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add a token exchange middleware
4 participants