Introduction
In the OAuth2 authorization code flow, the identity provider returns a short-lived authorization code that the application must exchange for an access token. This code typically expires within 60-300 seconds. If the exchange is delayed -- due to network latency, server processing time, or redirect chain issues -- the code expires and the token request fails, requiring the user to restart the authentication flow.
Symptoms
- User redirected back to application after login but authentication fails
- Application logs show
invalid_grantorauthorization code expirederror - Token endpoint returns HTTP 400 with
error: invalid_grant - User must re-authenticate by clicking login again
- Error message:
{"error":"invalid_grant","error_description":"The authorization code has expired"}
Common Causes
- Application server processing delay between receiving the code and exchanging it
- Network latency between the application server and the identity provider
- User taking too long between authorization and redirect back to the application
- Authorization code used twice (replay), which invalidates the first use
- Identity provider configured with a very short code expiration time
Step-by-Step Fix
- 1.Check the token exchange error details: Confirm the code expired.
- 2.```bash
- 3.# Check the token endpoint response
- 4.curl -X POST https://auth.example.com/oauth/token \
- 5.-d "grant_type=authorization_code" \
- 6.-d "code=AUTH_CODE" \
- 7.-d "redirect_uri=https://app.example.com/callback" \
- 8.-d "client_id=CLIENT_ID" \
- 9.-d "client_secret=CLIENT_SECRET"
- 10.# Response: {"error":"invalid_grant","error_description":"code has expired"}
- 11.
` - 12.Optimize the callback handler to exchange the code immediately: Minimize delay.
- 13.```javascript
- 14.// Node.js Express - exchange code immediately in the callback route
- 15.app.get('/callback', async (req, res) => {
- 16.const { code } = req.query;
- 17.// Exchange immediately, before any other processing
- 18.const tokenResponse = await fetch('https://auth.example.com/oauth/token', {
- 19.method: 'POST',
- 20.headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
- 21.body: new URLSearchParams({
- 22.grant_type: 'authorization_code',
- 23.code,
- 24.redirect_uri: process.env.REDIRECT_URI,
- 25.client_id: process.env.CLIENT_ID,
- 26.client_secret: process.env.CLIENT_SECRET,
- 27.}),
- 28.});
- 29.// Handle response...
- 30.});
- 31.
` - 32.Configure appropriate code expiration on the identity provider: Ensure adequate time.
- 33.
` - 34.# Identity provider configuration:
- 35.# Set authorization_code expiration to at least 300 seconds (5 minutes)
- 36.# This provides enough time for network latency and server processing
- 37.
` - 38.Implement automatic re-authentication on code expiration: Handle the error gracefully.
- 39.```javascript
- 40.if (tokenResponse.error === 'invalid_grant') {
- 41.// Redirect user back to login
- 42.return res.redirect('/login?error=session_expired&retry=true');
- 43.}
- 44.
` - 45.Monitor code exchange latency: Track and alert on slow exchanges.
- 46.```javascript
- 47.// Log the time between code receipt and token exchange
- 48.const startTime = Date.now();
- 49.const tokenResponse = await exchangeCode(code);
- 50.const exchangeTime = Date.now() - startTime;
- 51.logger.info({ exchangeTime }, 'OAuth code exchange duration');
- 52.
`
Prevention
- Exchange authorization codes immediately in the callback handler before any other processing
- Set authorization code expiration to at least 300 seconds on the identity provider
- Monitor code exchange latency and alert when it approaches the expiration threshold
- Implement automatic retry with re-authentication for expired code errors
- Use PKCE (Proof Key for Code Exchange) to prevent code interception attacks
- Optimize the callback endpoint to minimize server-side processing before the token exchange