PKCE Validation Timing Enhancement
Status: ✅ Implemented Date: 2026-01-07 Related: Gap Analysis § 4.1 - PKCE Validation Timing
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:
- Validate
code_challenge_methodis either "plain" or "S256" - Validate
code_challengelength is between 43-128 characters (RFC 7636 Section 4.2) - Validate
code_challengecontains only base64url characters (A-Z, a-z, 0-9, -, _) - Return OAuth-compliant error redirects with state parameter preservation
Implementation Details
Validation Rules
Location: src/routes/oauth-authorize.ts:64-123
// 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=xyzWithout redirect_uri (fallback):
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:
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
✓ 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 redirectsFiles Changed
| File | Lines Changed | Description |
|---|---|---|
| src/routes/oauth-authorize.ts | +64 | Added PKCE validation logic |
| src/routes/oauth-authorize.test.ts | +200 | Added 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
- Missing code_challenge_method - Returns error redirect with description
- Invalid method (not plain/S256) - Returns error with valid options
- Boundary conditions - Tests exact min (43) and max (128) lengths
- No redirect_uri - Falls back to JSON error response
- State parameter - Always preserved in error redirects for CSRF protection
- 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
- Metrics - Track PKCE validation failures by error type
- Rate limiting - Separate limits for validation failures
- Logging - Structured logs for security monitoring
- Plain method deprecation - Consider rejecting "plain" in future (S256 is recommended)
Known Limitations
- No code_verifier validation - Server doesn't validate the verifier matches challenge (handled by IdP)
- No enforcement - PKCE is optional, not required (consider making it mandatory in production)
- Entropy checking - Doesn't validate code_challenge has sufficient entropy
References
- RFC 7636: Proof Key for Code Exchange by OAuth Public Clients
- Implementation:
src/routes/oauth-authorize.ts - Tests:
src/routes/oauth-authorize.test.ts