How to fix unexpected_audience: JWT audience does not match in Supabase

SupabaseINTERMEDIATEHIGHUpdated Dec 18, 2025ยท 102 views

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.

Quick fix

Fixes unexpected_audience

// 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);
  1. Verify the current JWT audience claim
  2. Set correct audience when creating custom JWTs
  3. Verify audience in JWT validation logic
  4. Check for aud claim format changes
supabase โ€” error
// 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);
unexpected_audience: JWT audience does not match

What this error means

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.

Typical symptoms

  • Authentication fails with "unexpected_audience" error message
  • API requests to Supabase return 401 Unauthorized responses
  • Row Level Security (RLS) policies fail to execute correctly
  • Custom JWT tokens work locally but fail in production
  • Realtime subscriptions disconnect immediately after connecting
  • JWT verification succeeds in external tools but fails in Supabase

Common causes

  1. Creating custom JWTs without setting the "aud" claim to "authenticated"
  2. Using JWTs from a different Supabase project or authentication system
  3. Migrating from another JWT provider without updating audience claims
  4. Incorrectly configuring JWT audience in custom authentication flows
  5. Using service_role tokens where user tokens are expected (or vice versa)
  6. JWT verification logic checking for wrong audience value
  7. Recent changes to Supabase Auth that modified aud claim format (array vs string)

How to fix "unexpected_audience: JWT audience does not match"

1Verify the current JWT audience claim

First, decode your JWT token to inspect its claims. You can use jwt.io or decode it programmatically:

javascript
// 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".

2Set correct audience when creating custom JWTs

If you're generating custom JWTs for Supabase, ensure you include the correct audience claim:

javascript
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.

3Verify audience in JWT validation logic

If you're manually verifying JWTs, ensure your validation includes the correct audience check:

javascript
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);
}
python
# 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}")
4Check for aud claim format changes

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:

javascript
// 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}`);
}
python
# 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}")
5Ensure RLS policies match JWT audience

If you have Row Level Security policies that check authentication status, make sure they're compatible with your JWT audience:

sql
-- 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.

6Verify JWT secret matches your project

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:

bash
# .env.local
SUPABASE_JWT_SECRET=your-jwt-secret-here

Using the wrong secret will cause signature verification to fail, but using a secret from a different project can cause audience mismatch errors.

Advanced notes

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.

Frequently asked questions

What does "unexpected_audience" mean?
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.
How do I fix "unexpected_audience"?
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.
What causes "unexpected_audience"?
Creating custom JWTs without setting the "aud" claim to "authenticated" Using JWTs from a different Supabase project or authentication system Migrating from another JWT provider without updating audience claims
Is the "unexpected_audience" 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