Keycloak invalid_grant Error: Causes and Fixes

Guilliano Molaire Guilliano Molaire Updated March 24, 2026 11 min read

Last updated: March 2026

The invalid_grant error is one of the most common and frustrating errors you will encounter when integrating with Keycloak. It occurs during the token exchange step of the OAuth 2.0 / OIDC flow, and the error message itself gives you almost no information about what went wrong.

{
  "error": "invalid_grant",
  "error_description": "Code not valid"
}

This guide covers every known cause of invalid_grant errors in Keycloak, how to diagnose each one, and how to fix them. We will work through the causes from most common to least common.

Understanding When invalid_grant Occurs

The invalid_grant error happens during the token request (the second step of the authorization code flow). Here is the flow:

  1. Your application redirects the user to Keycloak’s authorization endpoint
  2. The user authenticates and Keycloak redirects back with an authorization code
  3. Your application exchanges the code for tokens at the token endpoint
  4. This is where invalid_grant occurs if something is wrong with the exchange

The error can also occur during:

  • Refresh token requests when the refresh token is invalid or expired
  • Token exchange (OAuth 2.0 Token Exchange extension)
  • Device authorization flow

Cause 1: Authorization Code Has Expired

The most common cause. Keycloak authorization codes have a short lifespan, 60 seconds by default.

Why it happens:

  • Your application takes too long to exchange the code (network latency, slow backend)
  • The user sits on the redirect for too long before the callback triggers
  • You are debugging and manually copying the code, which takes more than 60 seconds

How to verify:

Check the timestamp of the authorization response versus when the token request was made. If more than 60 seconds elapsed, this is your problem.

# Test the token exchange - if the code is expired, you'll get invalid_grant
curl -X POST "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token" 
  -H "Content-Type: application/x-www-form-urlencoded" 
  -d "grant_type=authorization_code" 
  -d "code=YOUR_AUTHORIZATION_CODE" 
  -d "client_id=your-client-id" 
  -d "client_secret=your-client-secret" 
  -d "redirect_uri=https://app.example.com/callback"

The fix:

Make sure your application exchanges the code immediately upon receiving it. If you need more time for development purposes, you can increase the code lifespan in the Keycloak admin console:

  1. Navigate to Realm Settings > Tokens
  2. Find OAuth 2.0 Device Code Lifespan and Client Login Timeout
  3. Increase the value (but keep it short in production for security)

Alternatively, in the client settings:

  1. Navigate to Clients > your client > Advanced tab
  2. Set Access Code Lifespan to a longer value

For production systems, keep the default 60 seconds and ensure your backend exchanges the code promptly.

Cause 2: Authorization Code Used More Than Once

The second most common cause. OAuth 2.0 authorization codes are single-use. If your application tries to exchange the same code twice, the second attempt fails with invalid_grant.

Why it happens:

  • Your frontend retries the callback request (browser refresh, React strict mode double-rendering)
  • A load balancer retries the request to a different backend instance
  • Your application logic has a bug that processes the callback twice

How to verify:

Check your application logs for duplicate token exchange requests. In Keycloak’s server logs, you will see:

WARN [org.keycloak.protocol.oidc.endpoints.TokenEndpoint] Code already used

The fix:

Handle the callback idempotently. After a successful token exchange, store the session and redirect. If the callback is hit again, check for an existing session before attempting another exchange.

// Next.js example - prevent double exchange
export async function GET(request) {
  const { searchParams } = new URL(request.url);
  const code = searchParams.get('code');
  const state = searchParams.get('state');

  // Check if we already have a session for this state
  const existingSession = await getSessionByState(state);
  if (existingSession) {
    return NextResponse.redirect('/dashboard');
  }

  // Exchange the code
  const tokens = await exchangeCode(code);
  await createSession(state, tokens);
  return NextResponse.redirect('/dashboard');
}

In React with Strict Mode (development only), components render twice. If your callback component triggers the token exchange on mount, it will fire twice. Wrap the exchange in a ref guard:

import { useEffect, useRef } from 'react';

function Callback() {
  const exchanged = useRef(false);

  useEffect(() => {
    if (exchanged.current) return;
    exchanged.current = true;

    const code = new URLSearchParams(window.location.search).get('code');
    if (code) {
      exchangeCodeForTokens(code);
    }
  }, []);

  return <div>Processing login...</div>;
}

Cause 3: Clock Skew Between Servers

The problem:

Keycloak validates timestamps in authorization codes and tokens. If the clock on your application server is significantly different from the clock on your Keycloak server, tokens and codes can appear expired or not-yet-valid.

How to verify:

# Check the time on your application server
date -u

# Check the time on your Keycloak server
docker exec keycloak date -u

# Compare the two - if they differ by more than a few seconds, you have clock skew

The fix:

Ensure all servers use NTP (Network Time Protocol) to synchronize their clocks:

# Check NTP sync status on Linux
timedatectl status

# Force NTP sync
sudo systemctl restart systemd-timesyncd

If you are running Keycloak in Docker, the container inherits the host’s clock. Make sure the Docker host is time-synchronized.

On Skycloak managed hosting, time synchronization is handled automatically across all infrastructure.

Cause 4: Refresh Token Has Expired or Been Revoked

The problem:

When using grant_type=refresh_token, the invalid_grant error means the refresh token is no longer valid. This can happen because:

  • The refresh token has exceeded its maximum lifetime
  • The user’s session was terminated (admin action, session timeout, or user logout)
  • The refresh token was revoked
  • Offline tokens were disabled after the token was issued

How to verify:

Inspect the refresh token to check its expiration. You can decode it with our JWT Token Analyzer:

# Decode a JWT refresh token (they're JWTs in Keycloak)
echo "YOUR_REFRESH_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool

Look at the exp (expiration) and iat (issued at) claims. Compare exp with the current Unix timestamp:

# Current Unix timestamp
date +%s

# If exp < current timestamp, the token is expired

The fix:

Adjust token lifetimes in Keycloak’s admin console:

  1. Navigate to Realm Settings > Tokens
  2. Key settings:
    • SSO Session Idle: how long a session can be idle (default: 30 minutes)
    • SSO Session Max: maximum session lifetime (default: 10 hours)
    • Client Session Idle: per-client session idle timeout
    • Client Session Max: per-client maximum session lifetime

For offline tokens (long-lived refresh tokens):

  1. Navigate to Realm Settings > Tokens
  2. Set Offline Session Idle and Offline Session Max Lifespan
  3. Enable offline access in your client’s scope configuration

Your application should handle expired refresh tokens gracefully by redirecting the user to re-authenticate:

async function refreshAccessToken(refreshToken) {
  const response = await fetch(
    'https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token',
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'refresh_token',
        refresh_token: refreshToken,
        client_id: 'your-client-id',
      }),
    }
  );

  if (!response.ok) {
    const error = await response.json();
    if (error.error === 'invalid_grant') {
      // Refresh token expired - redirect to login
      window.location.href = '/login';
      return null;
    }
    throw new Error(`Token refresh failed: ${error.error_description}`);
  }

  return response.json();
}

For a deeper dive into token lifecycle patterns, see our post on JWT token lifecycle management.

Cause 5: Wrong Client Secret

The problem:

If you send the wrong client_secret in the token request, Keycloak returns invalid_grant (or sometimes unauthorized_client, depending on the configuration).

How to verify:

Test with a curl command using the correct client secret from the Keycloak admin console:

# Get the client secret from admin console:
# Clients > your-client > Credentials > Client secret

curl -X POST "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token" 
  -H "Content-Type: application/x-www-form-urlencoded" 
  -d "grant_type=authorization_code" 
  -d "code=YOUR_CODE" 
  -d "client_id=your-client-id" 
  -d "client_secret=CORRECT_SECRET_HERE" 
  -d "redirect_uri=https://app.example.com/callback"

The fix:

  1. Open the Keycloak admin console
  2. Navigate to Clients > your client > Credentials
  3. Copy the Client secret
  4. Update your application’s environment variables

Be careful with secret rotation. If you regenerate the client secret in Keycloak, all existing applications using the old secret will start failing with invalid_grant.

Cause 6: PKCE Code Verifier Mismatch

The problem:

If your client uses PKCE (Proof Key for Code Exchange), the code_verifier sent during the token exchange must match the code_challenge sent during the authorization request. If they do not match, Keycloak returns invalid_grant.

Why it happens:

  • Your application generates a new code verifier on every request instead of storing and reusing the one from the authorization request
  • The code verifier is stored in a session or cookie that gets lost (e.g., cross-domain redirect, cleared cookies)
  • The code challenge method does not match (S256 vs plain)

How to verify:

PKCE uses two values:

  1. code_verifier: a random string generated by your app (43-128 characters)
  2. code_challenge: derived from the verifier (SHA-256 hash, base64url-encoded)

Check that your application:

  • Generates the verifier once at the start of the flow
  • Stores it (in session, cookie, or local storage)
  • Retrieves the same verifier for the token exchange
// Correct PKCE implementation
function generateCodeVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64URLEncode(array);
}

async function generateCodeChallenge(verifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const hash = await crypto.subtle.digest('SHA-256', data);
  return base64URLEncode(new Uint8Array(hash));
}

function base64URLEncode(buffer) {
  return btoa(String.fromCharCode(...buffer))
    .replace(/+/g, '-')
    .replace(///g, '_')
    .replace(/=+$/, '');
}

// Store verifier before redirecting to Keycloak
const verifier = generateCodeVerifier();
sessionStorage.setItem('pkce_verifier', verifier);
const challenge = await generateCodeChallenge(verifier);

// Authorization request
const authUrl = `https://keycloak.example.com/realms/myrealm/protocol/openid-connect/auth?` +
  `client_id=my-spa&` +
  `redirect_uri=${encodeURIComponent('https://app.example.com/callback')}&` +
  `response_type=code&` +
  `scope=openid&` +
  `code_challenge=${challenge}&` +
  `code_challenge_method=S256`;

// After callback - retrieve the SAME verifier
const storedVerifier = sessionStorage.getItem('pkce_verifier');
// Use storedVerifier in the token exchange

The fix:

Ensure the code verifier persists across the redirect. For SPAs, sessionStorage is the most reliable option. For server-side applications, use the server session.

For more on PKCE with Keycloak, see our guide on securing React apps with Keycloak OIDC PKCE. For background on OAuth 2.1 changes (which make PKCE mandatory), see upcoming changes in OAuth 2.1.

Cause 7: Redirect URI Mismatch in Token Request

The problem:

The redirect_uri in the token request must exactly match the redirect_uri used in the authorization request. If they differ, Keycloak returns invalid_grant.

This is a subtle variant of the redirect URI mismatch error. The authorization request might succeed (because the redirect URI matches Keycloak’s configured valid redirect URIs), but the token exchange fails because the two redirect URIs do not match each other.

How to verify:

Compare the redirect_uri from your authorization request with the one in your token request. They must be byte-for-byte identical.

The fix:

Use the same redirect URI construction logic for both requests. Ideally, define the redirect URI in one place and reference it in both the authorization and token exchange:

# Python example
REDIRECT_URI = "https://app.example.com/callback"

# Authorization request
auth_url = (
    f"{KEYCLOAK_URL}/realms/{REALM}/protocol/openid-connect/auth"
    f"?client_id={CLIENT_ID}"
    f"&redirect_uri={urllib.parse.quote(REDIRECT_URI, safe='')}"
    f"&response_type=code"
    f"&scope=openid"
)

# Token exchange - use the SAME REDIRECT_URI
token_response = requests.post(
    f"{KEYCLOAK_URL}/realms/{REALM}/protocol/openid-connect/token",
    data={
        "grant_type": "authorization_code",
        "code": auth_code,
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "redirect_uri": REDIRECT_URI,  # Must match exactly
    },
)

Cause 8: Session Already Terminated

The problem:

If the user’s Keycloak session was terminated between the authorization request and the token exchange, the authorization code becomes invalid because the session it was bound to no longer exists.

Why it happens:

  • An administrator manually terminated the user’s session
  • Another application called the logout endpoint, ending the SSO session
  • The SSO session timed out between the two steps (unlikely with default settings but possible with very short timeouts)

How to verify:

Check Keycloak’s event log for logout or session termination events around the time of the failure:

  1. Navigate to Events > Login Events in the admin console
  2. Filter by the user or client
  3. Look for LOGOUT or LOGOUT_ERROR events near the timestamp of the invalid_grant

Keycloak’s audit logging and session management features make this easy to investigate.

The fix:

This is usually a timing issue. Your application should handle invalid_grant gracefully by redirecting the user to re-authenticate:

try {
  const tokens = await exchangeCode(authorizationCode);
  // Proceed with authenticated session
} catch (error) {
  if (error.response?.data?.error === 'invalid_grant') {
    // Session was invalidated - start fresh
    redirectToLogin();
  }
}

Cause 9: Client Credentials Sent Wrong Way

The problem:

Keycloak clients can be configured to accept credentials in the request body (client_id + client_secret as POST parameters) or as a Basic auth header. If you send credentials using the wrong method, the token exchange can fail.

How to verify:

Check your client’s authentication method in Keycloak:

  1. Navigate to Clients > your client > Credentials
  2. Check Client Authenticator: it will be either “Client Id and Secret” or another method

The fix:

Match your token request to the configured method:

# Method 1: Client credentials in request body (most common)
curl -X POST "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token" 
  -H "Content-Type: application/x-www-form-urlencoded" 
  -d "grant_type=authorization_code" 
  -d "code=YOUR_CODE" 
  -d "client_id=your-client-id" 
  -d "client_secret=your-client-secret" 
  -d "redirect_uri=https://app.example.com/callback"

# Method 2: Basic auth header
curl -X POST "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token" 
  -H "Content-Type: application/x-www-form-urlencoded" 
  -u "your-client-id:your-client-secret" 
  -d "grant_type=authorization_code" 
  -d "code=YOUR_CODE" 
  -d "redirect_uri=https://app.example.com/callback"

Reading Keycloak Server Logs

When the error response does not give you enough detail, Keycloak’s server logs are your best friend. Enable debug logging for the token endpoint:

# Keycloak 17+ (Quarkus)
bin/kc.sh start-dev --log-level=org.keycloak.protocol.oidc.endpoints.TokenEndpoint:DEBUG

Common log messages and what they mean:

Log Message Cause
Code not valid Code expired, already used, or from wrong session
Invalid code verifier PKCE verifier does not match the challenge
Code is expired Authorization code exceeded its lifespan
Invalid client credentials Wrong client secret
Session not active User’s session was terminated
Stale token Refresh token belongs to an old session

Diagnostic Flowchart

When you hit invalid_grant, follow this decision tree:

  1. Is this a token exchange (authorization code) or refresh token request?

    • If refresh token: check token expiration, session status, and whether the token was revoked
    • If authorization code: continue to step 2
  2. How much time elapsed between the authorization response and the token request?

    • More than 60 seconds: code expired (Cause 1)
    • Less than 60 seconds: continue to step 3
  3. Is this the first time the code is being used?

    • No: code reuse (Cause 2)
    • Yes: continue to step 4
  4. Does the redirect_uri in the token request match the authorization request?

    • No: redirect URI mismatch (Cause 7)
    • Yes: continue to step 5
  5. Is the client using PKCE?

    • Yes: verify code_verifier matches code_challenge (Cause 6)
    • No: continue to step 6
  6. Is the client secret correct?

    • No: wrong secret (Cause 5)
    • Yes: check server logs for specific error, check clock skew (Cause 3)

Wrapping Up

The invalid_grant error is frustrating because it is a catch-all for multiple distinct problems. The key to debugging it efficiently is to narrow down the cause systematically: check the timing first, verify the code is being used only once, confirm the credentials, and then look at edge cases like PKCE and clock skew.

For most applications, implementing proper error handling and logging around the token exchange will save significant debugging time. Log the exact parameters you send (redacting secrets) and compare them against what Keycloak expects.

If you are building a new integration with Keycloak, our JWT Token Analyzer can help you verify that your tokens contain the expected claims. For SAML-based integrations, check out the SAML Decoder.

Need a Keycloak instance for development and testing? Use our Keycloak Docker Compose Generator for a quick local setup, or try Skycloak for a managed production environment with built-in monitoring and SLA guarantees.

Guilliano Molaire
Written by Guilliano Molaire Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Ready to simplify your authentication?

Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.

© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman