How to fix bad_jwt: JWT sent in Authorization header is not valid in Supabase

SupabaseINTERMEDIATEHIGHUpdated Dec 22, 2025· 200 views

Supabase rejects requests with a bad_jwt error when the JWT token in the Authorization header cannot be validated. This typically happens when using the wrong token type, an expired token, or a malformed JWT structure. This guide shows how to identify and fix JWT validation issues.

Quick fix

Fixes bad_jwt

// ❌ Wrong - using API key as bearer token
const { data } = await fetch('/api/data', {
  headers: {
    'Authorization': 'Bearer <YOUR_ANON_KEY>'  // This will fail
  }
});

// ✅ Correct - using Auth access token from session
const { data: { session } } = await supabase.auth.getSession();
const { data } = await fetch('/api/data', {
  headers: {
    'Authorization': `Bearer ${session?.access_token}`  // This works
  }
});
  1. Verify you are using an Auth access token, not an API key
  2. Check if the token has expired and refresh if needed
  3. Validate the JWT structure and signature
  4. Verify system time is synchronized
supabase — error
// ❌ Wrong - using API key as bearer token
const { data } = await fetch('/api/data', {
headers: {
'Authorization': 'Bearer <YOUR_ANON_KEY>' // This will fail
}
});
// ✅ Correct - using Auth access token from session
const { data: { session } } = await supabase.auth.getSession();
const { data } = await fetch('/api/data', {
headers: {
'Authorization': `Bearer ${session?.access_token}` // This works
}
});
bad_jwt: JWT sent in Authorization header is not valid

What this error means

Supabase Auth validates JSON Web Tokens (JWTs) on every authenticated request to ensure the caller has proper permissions. The "bad_jwt" error is returned when Supabase cannot successfully decode, parse, or verify the JWT signature sent in the Authorization header. Unlike server errors or network issues, this error means the token itself is invalid—it could be expired, malformed, signed with the wrong secret, or of the wrong type entirely. Supabase expects valid Auth access tokens (obtained from sign-in flows or session refresh), not API keys like the anon or service_role keys. When any part of the JWT validation chain fails—whether due to incorrect structure, expired timestamps, missing claims, or signature mismatch—Supabase immediately rejects the request with this error before executing any database queries or RLS policies.

Typical symptoms

  • API requests to Supabase fail with HTTP 401 and error message "bad_jwt: JWT sent in Authorization header is not valid".
  • Authentication worked previously but suddenly starts failing after token expiration or key rotation.
  • Using supabase.auth.getUser() or API calls with a JWT returns authentication errors even though credentials are correct.
  • Dashboard or client-side requests fail with JWT validation errors when computer system time is out of sync.
  • Direct API calls using curl or Postman with a copied JWT fail immediately without reaching database RLS policies.

Common causes

  1. Using a Supabase API key (anon or service_role) instead of an Auth access token in the Authorization header.
  2. The JWT token has expired (the 'exp' claim timestamp is in the past) and needs to be refreshed.
  3. The token structure is malformed—missing one of the three required parts (header, payload, signature) separated by dots.
  4. The JWT was signed with a different secret key than the one configured in the Supabase project settings.
  5. System clock on the client machine is significantly out of sync, causing timestamp validation failures.
  6. The token is missing required claims like 'sub', 'role', 'aud', or 'iss' that Supabase expects for validation.

How to fix "bad_jwt: JWT sent in Authorization header is not valid"

1Verify you are using an Auth access token, not an API key

The most common cause of this error is accidentally using the Supabase anon or service_role API key instead of a user's Auth access token. API keys look like JWTs but are meant for the apikey header, not the Authorization header.

Check your authentication code to ensure you're using the access token from the session:

typescript
// ❌ Wrong - using API key as bearer token
const { data } = await fetch('/api/data', {
  headers: {
    'Authorization': 'Bearer <YOUR_ANON_KEY>'  // This will fail
  }
});

// ✅ Correct - using Auth access token from session
const { data: { session } } = await supabase.auth.getSession();
const { data } = await fetch('/api/data', {
  headers: {
    'Authorization': `Bearer ${session?.access_token}`  // This works
  }
});

If you need to authenticate server-side, use the service_role key in the apikey header alongside a valid JWT, or use the Supabase service client which handles this automatically.

2Check if the token has expired and refresh if needed

JWTs include an exp (expiration) claim that determines when they become invalid. Decode your token at https://jwt.io to inspect the exp timestamp and verify it's still in the future.

If the token is expired, refresh it using Supabase Auth:

typescript
const { data: { session }, error } = await supabase.auth.refreshSession();

if (error) {
  console.error('Failed to refresh session:', error.message);
  // Redirect to login
  return;
}

// Use the new access token
const accessToken = session?.access_token;

Most Supabase client libraries automatically refresh tokens before they expire, but manual API calls or long-running sessions may need explicit refresh logic.

3Validate the JWT structure and signature

A valid JWT consists of three Base64URL-encoded parts separated by periods: header.payload.signature. Copy your token and paste it into https://jwt.io to verify the structure.

Check for these common structural issues:
- Missing or extra dots (should have exactly 2 dots)
- Truncated token (copied only part of it)
- Whitespace or line breaks in the token string
- URL encoding issues when passing token in query params

If the structure is valid, verify the signature matches your Supabase project:

typescript
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  'https://your-project.supabase.co',
  'your-anon-key'
);

// This verifies the token internally
const { data: { user }, error } = await supabase.auth.getUser(token);

if (error) {
  console.error('Invalid token:', error.message);
}

If signature verification fails, the token may have been signed with a different JWT secret or rotated signing keys.

4Verify system time is synchronized

JWT validation relies on timestamp comparisons between the token's exp claim and the server's current time. If your computer's clock is significantly out of sync, valid tokens may be rejected.

Visit https://time.is/ to check if your system time matches the actual time. If it's off by more than a few seconds:

On macOS:

bash
sudo systemsetup -setusingnetworktime on
sudo sntp -sS time.apple.com

On Ubuntu/Debian:

bash
sudo timedatectl set-ntp true
sudo systemctl restart systemd-timesyncd

On Windows:
Open Settings → Time & Language → Date & time → Sync now

After synchronizing, refresh your browser session and obtain a new access token.

5Inspect required JWT claims

Supabase requires specific claims in the JWT payload. Decode your token and verify these fields are present:

Required claims:
- sub: User ID (subject)
- role: Postgres role (usually 'authenticated' or 'anon')
- aud: Audience (should be 'authenticated')
- iss: Issuer (your Supabase project URL)
- exp: Expiration timestamp (Unix timestamp in seconds)

Example of a valid Supabase JWT payload:

json
{
  "aud": "authenticated",
  "exp": 1703980800,
  "iss": "https://your-project.supabase.co/auth/v1",
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "email": "user@example.com",
  "role": "authenticated",
  "session_id": "xyz789..."
}

If you're generating custom JWTs, ensure all required claims match the expected format. The role must be an existing Postgres role in your database.

Advanced notes

When using custom JWT providers or implementing server-side JWT generation, pay close attention to the signing algorithm. Supabase uses HS256 by default for the JWT secret, but newer projects may use RS256 with rotating signing keys. Check your project's JWT settings in the Supabase dashboard under Authentication → Settings → JWT Settings. If you recently rotated JWT secrets or signing keys, existing tokens signed with the old secret will fail validation until users refresh their sessions. During key rotation, there's typically a grace period where both old and new keys are accepted, but this window is limited. For production applications, implement automatic token refresh logic using supabase.auth.onAuthStateChange() to handle expiration and rotation seamlessly. If you're calling Supabase APIs from external services, consider using the service_role key with proper security measures rather than generating custom JWTs, as it bypasses RLS and provides full access to your database.

Frequently asked questions

What does "bad_jwt" mean?
Supabase Auth validates JSON Web Tokens (JWTs) on every authenticated request to ensure the caller has proper permissions. The "bad_jwt" error is returned when Supabase cannot successfully decode, parse, or verify the JWT signature sent in the Authorization header. Unlike server errors or network issues, this error means the token itself is invalid—it could be expired, malformed, signed with the wrong secret, or of the wrong type entirely.
How do I fix "bad_jwt"?
Supabase rejects requests with a bad_jwt error when the JWT token in the Authorization header cannot be validated. This typically happens when using the wrong token type, an expired token, or a malformed JWT structure. This guide shows how to identify and fix JWT validation issues.
What causes "bad_jwt"?
Using a Supabase API key (anon or service_role) instead of an Auth access token in the Authorization header. The JWT token has expired (the 'exp' claim timestamp is in the past) and needs to be refreshed. The token structure is malformed—missing one of the three required parts (header, payload, signature) separated by dots.
Is the "bad_jwt" error serious?
Yes. This is a high-severity error that usually blocks work until it is fixed, but the steps above resolve it.

Official resources & further reading