Skip to content

PKCE Validation Timing Enhancement

Status: ✅ Implemented Date: 2026-01-07 Related: Gap Analysis § 4.1 - PKCE Validation Timing

← Back to Enhancements


Problem Statement

The OAuth authorization endpoint (/oauth/authorize) was proxying requests to the IdP without validating PKCE parameters upfront. Invalid PKCE parameters (code_challenge, code_challenge_method) were only caught later at the token endpoint, after the user had already completed the full authorization flow.

Impact

  • Poor user experience - Users complete the entire OAuth flow before discovering PKCE errors
  • Wasted redirects - Unnecessary round trips to the IdP for invalid requests
  • Late error detection - PKCE validation errors only appear at token exchange, not authorization
  • Confusing error messages - Users see errors after successfully authenticating

Solution Overview

Implement early PKCE parameter validation at the /oauth/authorize endpoint before proxying to the IdP:

  1. Validate code_challenge_method is either "plain" or "S256"
  2. Validate code_challenge length is between 43-128 characters (RFC 7636 Section 4.2)
  3. Validate code_challenge contains only base64url characters (A-Z, a-z, 0-9, -, _)
  4. Return OAuth-compliant error redirects with state parameter preservation

Implementation Details

Validation Rules

Location: src/routes/oauth-authorize.ts:64-123

typescript
// 1. code_challenge_method validation
if (codeChallenge) {
  if (!codeChallengeMethod || !["plain", "S256"].includes(codeChallengeMethod)) {
    // Return error redirect with preserved state
  }

  // 2. code_challenge length validation (43-128 chars per RFC 7636)
  if (codeChallenge.length < 43 || codeChallenge.length > 128) {
    // Return error redirect
  }

  // 3. code_challenge format validation (base64url only)
  if (!/^[A-Za-z0-9_-]+$/.test(codeChallenge)) {
    // Return error redirect
  }
}

Error Handling

The implementation follows OAuth 2.0 error handling best practices:

With redirect_uri (preferred):

HTTP/1.1 302 Found
Location: https://example.com/callback?error=invalid_request&error_description=Invalid+code_challenge+length&state=xyz

Without redirect_uri (fallback):

json
HTTP/1.1 400 Bad Request
{
  "error": "invalid_request",
  "error_description": "Invalid code_challenge length. Must be 43-128 characters"
}

State Preservation

The implementation carefully preserves the state parameter in error redirects for CSRF protection:

typescript
const errorRedirect = redirectUri
  ? `${redirectUri}?error=invalid_request&error_description=${encodeURIComponent(message)}${
      req.query.state ? `&state=${encodeURIComponent(req.query.state as string)}` : ""
    }`
  : null;

Testing

Added comprehensive test coverage in src/routes/oauth-authorize.test.ts.

Test Cases

Valid PKCE parameters (2 tests):

  • ✅ Accept valid S256 code_challenge
  • ✅ Accept valid plain code_challenge

Invalid code_challenge_method (2 tests):

  • ✅ Reject missing code_challenge_method
  • ✅ Reject invalid code_challenge_method (not "plain" or "S256")

Invalid code_challenge length (4 tests):

  • ✅ Reject code_challenge < 43 characters
  • ✅ Reject code_challenge > 128 characters
  • ✅ Accept code_challenge = 43 characters (minimum)
  • ✅ Accept code_challenge = 128 characters (maximum)

Invalid code_challenge format (2 tests):

  • ✅ Reject code_challenge with invalid characters (+, /, =)
  • ✅ Accept code_challenge with all valid base64url characters

Error handling (2 tests):

  • ✅ Return JSON error when no redirect_uri provided
  • ✅ Preserve state parameter in error redirects

Total: 12 new tests for PKCE validation

Test Results

bash
 src/routes/oauth-authorize.test.ts (30 tests | 1 skipped) 80ms
 PKCE validation (12 tests)
 should accept valid PKCE parameters with S256 method
 should accept valid PKCE parameters with plain method
 should reject code_challenge without code_challenge_method
 should reject invalid code_challenge_method
 should reject code_challenge that is too short
 should reject code_challenge that is too long
 should reject code_challenge with invalid characters
 should return JSON error when no redirect_uri provided
 should accept code_challenge at minimum valid length (43 chars)
 should accept code_challenge at maximum valid length (128 chars)
 should accept code_challenge with base64url characters
 should preserve state parameter in error redirects

Files Changed

FileLines ChangedDescription
src/routes/oauth-authorize.ts+64Added PKCE validation logic
src/routes/oauth-authorize.test.ts+200Added 12 comprehensive test cases

Benefits

User Experience

  • Immediate error feedback - Users see PKCE errors before starting the OAuth flow
  • Clear error messages - Specific validation errors with suggestions
  • State preservation - CSRF protection maintained in error responses

Security

  • Early validation - Catch malformed PKCE parameters before IdP interaction
  • Reduced attack surface - Fewer invalid requests reach the IdP
  • RFC 7636 compliance - Proper base64url format and length validation

Performance

  • Reduced IdP load - Invalid requests rejected before proxy
  • Fewer wasted redirects - Users don't complete OAuth flow for invalid requests
  • Lower latency - Faster error responses without round trip to IdP

Configuration

No configuration required - PKCE validation is always enabled per RFC 7636.

RFC 7636 Requirements

From RFC 7636 Section 4.2:

code_challenge = base64url(SHA256(code_verifier))

Minimum length: 43 characters Maximum length: 128 characters Character set: [A-Z] / [a-z] / [0-9] / "-" / "_"


Edge Cases Handled

  1. Missing code_challenge_method - Returns error redirect with description
  2. Invalid method (not plain/S256) - Returns error with valid options
  3. Boundary conditions - Tests exact min (43) and max (128) lengths
  4. No redirect_uri - Falls back to JSON error response
  5. State parameter - Always preserved in error redirects for CSRF protection
  6. URL encoding - Error descriptions properly encoded for redirect URLs

Migration Notes

This is a backward-compatible enhancement:

  • Valid PKCE requests continue to work unchanged
  • Invalid PKCE requests now fail earlier with better error messages
  • No configuration changes required
  • Existing OAuth clients automatically benefit from improved error handling

Future Considerations

Potential Enhancements

  1. Metrics - Track PKCE validation failures by error type
  2. Rate limiting - Separate limits for validation failures
  3. Logging - Structured logs for security monitoring
  4. Plain method deprecation - Consider rejecting "plain" in future (S256 is recommended)

Known Limitations

  1. No code_verifier validation - Server doesn't validate the verifier matches challenge (handled by IdP)
  2. No enforcement - PKCE is optional, not required (consider making it mandatory in production)
  3. Entropy checking - Doesn't validate code_challenge has sufficient entropy

References


← Back to Enhancements

Released under the MIT License.