This error occurs when the "aud" (audience) claim in your JWT token doesn't match what Supabase expects during authentication validation. Supabase requires JWTs to have an audience claim set to "authenticated" for standard user sessions.
This error indicates a JWT validation failure where the audience claim in your token doesn't match Supabase's expected value. The "aud" (audience) claim identifies the intended recipients of the JWT and is a critical security measure to ensure tokens are used with the correct service. In Supabase, the audience claim serves multiple purposes: it groups users in multi-tenant deployments, ensures tokens aren't reused across different projects, and validates that authentication tokens are intended for your specific Supabase instance. When you receive this error, it means your JWT was either generated with the wrong audience value or your validation logic is checking for an incorrect audience. This commonly happens when creating custom JWTs for server-side operations, implementing custom authentication flows, or when migrating from another auth system. Supabase expects "authenticated" as the audience for standard user sessions and "realtime" for Realtime subscriptions with custom JWTs.
First, decode your JWT token to inspect its claims. You can use jwt.io or decode it programmatically:
// Using jose library
import { decodeJwt } from 'jose';
const token = 'your-jwt-token-here';
const claims = decodeJwt(token);
console.log('Audience claim:', claims.aud);
console.log('All claims:', claims);Check what value is currently in the aud field. For Supabase user sessions, it should be "authenticated".
If you're generating custom JWTs for Supabase, ensure you include the correct audience claim:
import { SignJWT } from 'jose';
import { createSecretKey } from 'crypto';
const jwtSecret = process.env.SUPABASE_JWT_SECRET!;
const secret = createSecretKey(Buffer.from(jwtSecret));
const token = await new SignJWT({
sub: userId,
role: 'authenticated',
aud: 'authenticated', // Required: must match Supabase's expectation
email: user.email,
})
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
.setIssuedAt()
.setExpirationTime('1h')
.sign(secret);For Realtime with custom JWTs, use aud: 'realtime' instead.
If you're manually verifying JWTs, ensure your validation includes the correct audience check:
import { jwtVerify } from 'jose';
try {
const { payload } = await jwtVerify(token, secret, {
audience: 'authenticated', // Must match the token's aud claim
issuer: process.env.SUPABASE_URL,
});
console.log('JWT verified successfully:', payload);
} catch (error) {
console.error('JWT verification failed:', error.message);
}# Using python-jose
from jose import jwt
try:
payload = jwt.decode(
token,
jwt_secret,
algorithms=['HS256'],
audience='authenticated', # Critical: must match token
issuer=supabase_url
)
except jwt.JWTClaimsError as e:
print(f"JWT claims validation failed: {e}")Supabase Auth recently changed the aud claim format from a string to an array in some cases (particularly with OAuth flows). Check if this affects your validation:
// Handle both string and array formats
const claims = decodeJwt(token);
const audience = Array.isArray(claims.aud) ? claims.aud[0] : claims.aud;
if (audience !== 'authenticated') {
throw new Error(`Invalid audience: ${audience}`);
}# Handle both formats in python-jose
import jwt
try:
payload = jwt.decode(
token,
jwt_secret,
algorithms=['HS256'],
options={"verify_aud": True}
)
# Handle array or string
aud = payload.get('aud')
if isinstance(aud, list):
aud = aud[0] if aud else None
if aud != 'authenticated':
raise ValueError(f"Invalid audience: {aud}")
except Exception as e:
print(f"Validation error: {e}")If you have Row Level Security policies that check authentication status, make sure they're compatible with your JWT audience:
-- Check current RLS policies
SELECT schemaname, tablename, policyname, qual
FROM pg_policies
WHERE schemaname = 'public';
-- Typical RLS policy for authenticated users
CREATE POLICY "Users can view their own data"
ON public.users
FOR SELECT
TO authenticated -- This role expects aud='authenticated'
USING (auth.uid() = id);The TO authenticated clause expects JWTs with aud: 'authenticated'. If your JWTs have a different audience, your RLS policies won't work correctly.
Ensure you're using the correct JWT secret from your Supabase project settings:
1. Go to your Supabase Dashboard
2. Navigate to Settings → API
3. Copy the "JWT Secret" (not the anon or service_role key)
4. Update your environment variables:
# .env.local
SUPABASE_JWT_SECRET=your-jwt-secret-hereUsing the wrong secret will cause signature verification to fail, but using a secret from a different project can cause audience mismatch errors.
Multi-tenant Deployments: If you're running a multi-tenant application where different customers use separate Supabase projects, ensure you're mapping the correct JWT audience to each tenant's project. The audience mechanism is designed to prevent token reuse across projects.
Custom Signing Keys: If you're using Supabase's "Bring Your Own Key" feature with custom JWT signing keys, be aware that there have been reported issues where properly signed JWTs still return "Invalid API key" errors. This is a known limitation as of late 2024 and may require using the default signing method.
Service Role vs User Tokens: Service role JWTs (created with your service_role key) have elevated privileges and bypass RLS. They typically don't require an audience claim, but user-level JWTs always do. Never expose service_role JWTs to client-side code.
OAuth Provider Changes: When authenticating through OAuth providers (Google, GitHub, etc.), Supabase manages the JWT creation for you. Recent versions changed the aud claim from a string to an array format, which can break custom JWT validators. Always handle both formats in your validation logic.
Debugging with jwt.io: When troubleshooting, paste your JWT into jwt.io to inspect the header, payload, and signature. This helps identify whether the issue is with the audience claim, signature verification, or other JWT properties.
Performance Consideration: JWT validation happens on every request. If you're seeing this error intermittently, check for race conditions where tokens might be refreshed mid-request or cached with stale audience values.
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