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.
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.
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:
// ❌ 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.
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:
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.
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:
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.
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:
sudo systemsetup -setusingnetworktime on
sudo sntp -sS time.apple.comOn Ubuntu/Debian:
sudo timedatectl set-ntp true
sudo systemctl restart systemd-timesyncdOn Windows:
Open Settings → Time & Language → Date & time → Sync now
After synchronizing, refresh your browser session and obtain a new access token.
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:
{
"aud": "authenticated",
"exp": 1703980800,
"iss": "https://your-project.supabase.co/auth/v1",
"sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"email": "[email protected]",
"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.
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.
email_address_not_authorized: Email sending to this address is not authorized
Email address not authorized for sending in Supabase Auth
reauthentication_needed: Reauthentication required for security-sensitive actions
Reauthentication required for security-sensitive actions
no_authorization: No authorization header was provided
How to fix "no authorization header was provided" in Supabase
otp_expired: OTP has expired
How to fix 'otp_expired: OTP has expired' in Supabase
bad_oauth_state: OAuth state parameter is missing or invalid
How to fix 'bad_oauth_state: OAuth state parameter missing' in Supabase