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_grant or authorization code expired error
  • 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. 1.Check the token exchange error details: Confirm the code expired.
  2. 2.```bash
  3. 3.# Check the token endpoint response
  4. 4.curl -X POST https://auth.example.com/oauth/token \
  5. 5.-d "grant_type=authorization_code" \
  6. 6.-d "code=AUTH_CODE" \
  7. 7.-d "redirect_uri=https://app.example.com/callback" \
  8. 8.-d "client_id=CLIENT_ID" \
  9. 9.-d "client_secret=CLIENT_SECRET"
  10. 10.# Response: {"error":"invalid_grant","error_description":"code has expired"}
  11. 11.`
  12. 12.Optimize the callback handler to exchange the code immediately: Minimize delay.
  13. 13.```javascript
  14. 14.// Node.js Express - exchange code immediately in the callback route
  15. 15.app.get('/callback', async (req, res) => {
  16. 16.const { code } = req.query;
  17. 17.// Exchange immediately, before any other processing
  18. 18.const tokenResponse = await fetch('https://auth.example.com/oauth/token', {
  19. 19.method: 'POST',
  20. 20.headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  21. 21.body: new URLSearchParams({
  22. 22.grant_type: 'authorization_code',
  23. 23.code,
  24. 24.redirect_uri: process.env.REDIRECT_URI,
  25. 25.client_id: process.env.CLIENT_ID,
  26. 26.client_secret: process.env.CLIENT_SECRET,
  27. 27.}),
  28. 28.});
  29. 29.// Handle response...
  30. 30.});
  31. 31.`
  32. 32.Configure appropriate code expiration on the identity provider: Ensure adequate time.
  33. 33.`
  34. 34.# Identity provider configuration:
  35. 35.# Set authorization_code expiration to at least 300 seconds (5 minutes)
  36. 36.# This provides enough time for network latency and server processing
  37. 37.`
  38. 38.Implement automatic re-authentication on code expiration: Handle the error gracefully.
  39. 39.```javascript
  40. 40.if (tokenResponse.error === 'invalid_grant') {
  41. 41.// Redirect user back to login
  42. 42.return res.redirect('/login?error=session_expired&retry=true');
  43. 43.}
  44. 44.`
  45. 45.Monitor code exchange latency: Track and alert on slow exchanges.
  46. 46.```javascript
  47. 47.// Log the time between code receipt and token exchange
  48. 48.const startTime = Date.now();
  49. 49.const tokenResponse = await exchangeCode(code);
  50. 50.const exchangeTime = Date.now() - startTime;
  51. 51.logger.info({ exchangeTime }, 'OAuth code exchange duration');
  52. 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