new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

Supabase · Auth · all subjects

authentication/jwts

114 notes in this subject, read out of this brain and free to use. This is page 1 of 2.

JWT as authentication mechanism

Supabase Auth uses JSON Web Tokens (JWTs) for authentication.

Access token amr claim structure

The amr (Authentication Methods Reference) claim is an array of objects. Each object contains a method (string, enum of: oauth, password, otp, totp, recovery, invite, sso/saml, magiclink, email/signup, email_change, token_refresh, anonymous) and a timestamp (integer, Unix timestamp).

Access token aal claim values

The aal (Authentication Assurance Level) claim in an access token can have the values: aal1, aal2, or aal3.

Access token role claim values

The role claim in an access token can have the values: anon or authenticated.

Custom access token hook output

The custom access token hook must return an object with a single field: claims (object), containing the updated claims after the hook has been run. Return this only if the hook processed the input without errors.

Custom access token hook inputs

The custom access token hook receives three inputs: user_id (string, unique identifier for the user attempting to sign in), claims (object, claims which are included in the access token), and authentication_method (string, the authentication method used to request the access token). Possible authentication_method values are: oauth, password, otp, totp, recovery, invite, sso/saml, magiclink, email/signup, email_change, token_refresh, oauth_provider/authorization_code, anonymous.

Custom access token hook overview

The custom access token hook runs before a token is issued and allows you to add additional claims based on the authentication method used. Claims returned must conform to Supabase Auth specification, which will check for these claims after the hook is run and return an error if they are not present.

Required access token claims

The following claims are required on an access token: iss, aud, exp, iat, sub, role, aal, session_id, email, phone, is_anonymous.

Optional access token claims

The following claims are optional on an access token: jti, nbf, app_metadata, user_metadata, amr.

Restrict access to SSO users HTTP hook example

```javascript import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0' import { readAll } from 'https://deno.land/std/io/read_all.ts' import * as base64 from 'https://denopkg.com/chiefbiiko/base64/mod.ts' Deno.serve(async (req) => { const payload = await req.text() const base64_secret = Deno.env.get('CUSTOM_ACCESS_TOKEN_SECRET').replace('v1,whsec_', '') const headers = Object.fromEntries(req.headers) const wh = new Webhook(base64_secret) try { const { user_id, claims, authentication_method } = wh.verify(payload, headers) // Check the condition const allowedEmails = ['myemail@company.com', 'example@company.com'] if (authentication_method === 'sso/saml' || allowedEmails.includes(claims.email)) { return new Response( JSON.stringify({ claims, }), { status: 200, headers: { 'Content-Type': 'application/json', }, } ) } else { return new Response( JSON.stringify({ error: 'Unauthorized', }), { status: 500, headers: { 'Content-Type': 'application/json', }, } ) } } catch (error) { return new Response( JSON.stringify({ error: `Failed to process the request: ${error}`, }), { status: 500, headers: { 'Content-Type': 'application/json', }, } ) } }) ```

Add claim HTTP hook example

```javascript import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0' import { readAll } from 'https://deno.land/std/io/read_all.ts' import * as base64 from 'https://denopkg.com/chiefbiiko/base64/mod.ts' Deno.serve(async (req) => { const payload = await req.text() const base64_secret = Deno.env.get('CUSTOM_ACCESS_TOKEN_SECRET').replace('v1,whsec_', '') const headers = Object.fromEntries(req.headers) const wh = new Webhook(base64_secret) try { const { user_id, claims, authentication_method } = wh.verify(payload, headers) if (claims.app_metadata && claims.app_metadata.role) { claims['role'] = claims.app_metadata.role } return new Response( JSON.stringify({ claims, }), { status: 200, headers: { 'Content-Type': 'application/json', }, } ) } catch (error) { return new Response( JSON.stringify({ error: `Failed to process the request: ${error}`, }), { status: 500, headers: { 'Content-Type': 'application/json', }, } ) } }) ```

Add claim via HTTP custom access token hook

You can add or modify claims in an HTTP-based custom access token hook. The hook receives the webhook payload containing user_id, claims, and authentication_method, verifies the webhook signature using the CUSTOM_ACCESS_TOKEN_SECRET, processes the claims, and returns them in a JSON response.

Restrict access to SSO users SQL hook example

```sql create or replace function public.restrict_application_access(event jsonb) returns jsonb language plpgsql as $function$ declare authentication_method text; email_claim text; allowed_emails text[] := array['myemail@company.com', 'example@company.com']; begin -- Extract email claim and authentication method email_claim = event->'claims'->>'email'; authentication_method = event->'authentication_method'; -- Authentication methods come double quoted (e.g. "otp") authentication_method = replace(authentication_method, '"', ''); if email_claim ilike '%@supabase.io' or authentication_method = 'sso/saml' or email_claim = any(allowed_emails) then return event; end if; -- If none of the conditions are met, return an error return jsonb_build_object( 'error', jsonb_build_object( 'http_code', 403, 'message', 'Staging access is only allowed to team members. Please use your @company.com account instead' ) ); end; $function$ ; grant execute on function public.restrict_application_access to supabase_auth_admin; revoke execute on function public.restrict_application_access from authenticated, anon, public; ```

Add admin role SQL hook example

```sql create table profiles ( user_id uuid not null primary key references auth.users (id), is_admin boolean not null default false ); create or replace function public.custom_access_token_hook(event jsonb) returns jsonb language plpgsql as $$ declare claims jsonb; is_admin boolean; begin -- Check if the user is marked as admin in the profiles table select is_admin into is_admin from profiles where user_id = (event->>'user_id')::uuid; -- Proceed only if the user is an admin if is_admin then claims := event->'claims'; -- Check if 'app_metadata' exists in claims if jsonb_typeof(claims->'app_metadata') is null then -- If 'app_metadata' does not exist, create an empty object claims := jsonb_set(claims, '{app_metadata}', '{}'); end if; -- Set a claim of 'admin' claims := jsonb_set(claims, '{app_metadata, admin}', 'true'); -- Update the 'claims' object in the original event event := jsonb_set(event, '{claims}', claims); end if; -- Return the modified or original event return event; end $$; grant all on table public.profiles to supabase_auth_admin; revoke all on table public.profiles from authenticated, anon, public; ```

Add admin role to access token SQL hook

You can grant an admin claim to registered admin users in their access token. First create a profiles table with an is_admin boolean field, then in the custom access token hook, check if the user is marked as admin and set a claim of admin in app_metadata. The hook also checks if app_metadata exists in claims and creates an empty object if it does not before setting the admin claim.

Reduce JWT size with custom access token hook

The size of a JWT can become problematic, especially with Server-Side Rendering frameworks. Common situations include large user names, email addresses or phone numbers, too many default claims from OAuth providers, or large avatar URLs. You can reduce JWT size by defining a Custom Access Token hook that filters the token to include only specified claims, provided all required claims remain present.

Minimal JWT SQL hook example

This SQL function creates a custom access token hook that reduces JWT size by only including specified claims. It iterates through a whitelist of claim names ('iss', 'aud', 'exp', 'iat', 'sub', 'role', 'aal', 'session_id', 'email', 'phone', 'is_anonymous') and copies only those claims from the original token to the new token: ```sql create or replace function public.custom_access_token_hook(event jsonb) returns jsonb language plpgsql as $$ declare original_claims jsonb; new_claims jsonb; claim text; begin original_claims = event->'claims'; new_claims = '{}'::jsonb; foreach claim in array array[ -- add claims you want to keep here 'iss', 'aud', 'exp', 'iat', 'sub', 'role', 'aal', 'session_id', 'email', 'phone', 'is_anonymous' ] loop if original_claims ? claim then -- original_claims contains one of the listed claims, set it on new_claims new_claims = jsonb_set(new_claims, array[claim], original_claims->claim); end if; end loop; return jsonb_build_object('claims', new_claims); end $$; ```

JWT structure: header, payload, signature

Supabase JWTs follow standard JWT structure with three parts. The header contains algorithm and key information. The payload contains the claims including user data and metadata. The signature is the cryptographic signature for verification.

Required JWT claims in Supabase auth tokens

All Supabase JWTs must contain these required claims: iss (string, issuer - the entity that issued the JWT, example https://project-ref.supabase.co/auth/v1), aud (string or string array, audience - intended recipient, "authenticated" or "anon"), exp (number, expiration time as Unix timestamp), iat (number, issued at as Unix timestamp), sub (string, subject - user ID as UUID), role (string, user's role - "authenticated", "anon", or "service_role"), aal (string, authenticator assurance level - "aal1" or "aal2"), session_id (string, unique session identifier), email (string, user's email address), phone (string, user's phone number), is_anonymous (boolean, whether user is anonymous).

Optional JWT claims in Supabase auth tokens

These claims may be present in Supabase JWTs depending on authentication context: jti (string, JWT ID - unique identifier for the JWT), nbf (number, not before - Unix timestamp before which token is invalid), app_metadata (object, application-specific user data, example {"provider": "email"}), user_metadata (object, user-specific data, example {"name": "John Doe"}), amr (array, authentication methods reference - list of authentication methods used, example [{"method": "password", "timestamp": 1640991600}]).

Special JWT claim: ref (project reference)

The ref claim is a special claim containing the Supabase project reference identifier as a string. It appears only in anon and service_role tokens, not in authenticated user tokens. Example value: "abcdefghijklmnopqrst". This field is a reserved keyword in Rust and requires special handling during deserialization.

Authenticator assurance level (aal) values

The aal claim indicates authentication strength with two possible values: aal1 means single-factor authentication (password, OAuth, etc.), aal2 means multi-factor authentication (password + TOTP, etc.).

JWT role claim values

The role claim can have three values: "anon" for anonymous users used with RLS policies for public access, "authenticated" for authenticated users with standard user access, "service_role" for service role with admin privileges for server-side only use.

JWT audience (aud) claim values

The aud claim indicates token audience: "authenticated" for authenticated user tokens, "anon" for anonymous user tokens.

Authentication methods (amr.method) in JWT

The amr claim contains an array of authentication methods used. Possible method values are: "oauth" for OAuth provider authentication, "password" for email/password authentication, "otp" for one-time password, "totp" for time-based one-time password, "recovery" for account recovery, "invite" for invitation-based signup, "sso/saml" for SAML single sign-on, "magiclink" for magic link authentication, "email/signup" for email signup, "email_change" for email change, "token_refresh" for token refresh, "anonymous" for anonymous authentication.

Example: authenticated user JWT token structure

An authenticated user token contains: aal ("aal1"), amr array with method and timestamp, app_metadata with provider info, aud ("authenticated"), email, exp, iat, iss, phone, role ("authenticated"), session_id, sub, user_metadata, is_anonymous (false), and no ref field.

Example: anonymous user JWT token structure

An anonymous token is minimal and contains: iss ("supabase"), ref (project reference), role ("anon"), iat, exp. It lacks user identity claims like email, sub, and session_id.

Example: service role JWT token structure

A service role token contains: iss ("supabase"), ref (project reference), role ("service_role"), iat, exp. It is minimal like the anonymous token but with service_role instead of anon.

Rust JWT deserialization with reserved keyword handling

In Rust, the ref field is a reserved keyword. When deserializing JWTs, use serde's rename attribute: #[serde(rename = "ref")] above a field named project_ref with type Option<String>. This allows the Rust struct field to have a different name than the JSON field.

TypeScript/JavaScript JWT claims interface

A TypeScript interface for Supabase JWT claims includes required fields: iss (string), aud (string or string array), exp (number), iat (number), sub (string), role (string), aal (literal 'aal1' or 'aal2'), session_id (string), email (string), phone (string), is_anonymous (boolean). Optional fields: jti (string), nbf (number), app_metadata (Record<string, any>), user_metadata (Record<string, any>), amr (array of {method: string, timestamp: number}), ref (string, only in anon/service role tokens).

Python JWT claims dataclass structure

Python dataclasses for Supabase JWT claims use typing for flexibility. Required fields: iss (str), aud (Union[str, List[str]]), exp (int), iat (int), sub (str), role (str), aal (str), session_id (str), email (str), phone (str), is_anonymous (bool). Optional fields with default None: jti (Optional[str]), nbf (Optional[int]), app_metadata (Optional[Dict[str, Any]]), user_metadata (Optional[Dict[str, Any]]), amr (Optional[List[AmrEntry]] where AmrEntry has method and timestamp), ref (Optional[str], only in anon/service role tokens).

Go JWT claims struct with JSON tags

Go struct for JWT claims maps JSON fields using json tags: Iss, Aud (interface{} for string or []string), Exp, Iat (int64), Sub, Role, Aal, SessionID (mapped from session_id), Email, Phone, IsAnonymous (mapped from is_anonymous), Jti, Nbf (*int64, omitempty), AppMetadata, UserMetadata (map[string]interface{}, omitempty), Amr ([]AmrEntry with omitempty), Ref (*string, omitempty, only in anon/service role tokens).

JWT validation guidelines for servers

When implementing JWT validation on your server, follow these steps: 1) Check that all required fields are present. 2) Verify field types match expected types. 3) Check that exp timestamp is in the future. 4) Verify iss matches your Supabase project. 5) Validate aud matches expected audience. 6) Handle reserved keywords for languages like Rust.

JWT security best practices

Always validate the JWT signature before trusting any claims. Never expose service role tokens to client-side code. Validate all claims before trusting the JWT. Check token expiration on every request. Use HTTPS for all JWT transmission. Rotate JWT secrets regularly. Implement proper error handling for invalid tokens.

Supabase creates JWTs from API keys

Each publishable or secret API key is transformed on-the-fly into a short-lived JWT used to authorize access to data. These short-lived tokens are generally not directly accessible.

JWT role claim maps to Postgres role for RLS

The role claim in a JWT payload specifies which Postgres role to use when applying Row Level Security policies to that user's access.

Supabase Auth creates access token JWTs for signed-in users

Supabase Auth continuously issues a new JWT for each user session as long as the user remains signed in. These are short-lived and continuously reissued as the user interacts with Supabase APIs.

JWT signature purpose and verification

The JWT signature is a digital signature using either a shared secret (HMAC) or public-key cryptography. Its purpose is to verify the authenticity of the header and payload without requiring database access or Auth server liveness. Use supabase.auth.getClaims() or high-quality JWT verification libraries rather than implementing algorithms yourself.

JWT payload claims in Supabase

The JWT payload contains claims about the user. Key claims are: iss (the server that issued the token), exp (expiration timestamp), sub (the user ID), role (the Postgres role for Row Level Security), and email/phone (profile information). Other claims provide quick access to profile data without querying the database.

JWT header format and fields

The JWT header is Base64-URL encoded JSON containing: typ (the token type, usually "JWT"), alg (the cryptographic algorithm: HS256, ES256, or RS256), and kid (optional unique key identifier).

JWT structure: header, payload, signature

A JSON Web Token is a string with three parts separated by dots: <header>.<payload>.<signature>. Each part is a Base64-URL encoded JSON, or bytes for the signature.

JWT iss claim and JWKS discovery

The iss (issuer) claim identifies the server that issued the token. Appending /.well-known/jwks.json to the iss URL provides access to the public keys needed to verify the token.

JWT exp claim sets expiration time

The exp (expiration) claim sets a Unix timestamp after which the token should not be trusted and is considered expired, even if properly signed.

JWT sub claim is the user ID

The sub (subject) claim in a JWT payload is the unique ID of the user represented by the token.

How to send custom JWTs to Supabase client

Pass a custom JWT to the Supabase client library using the accessToken option, which can be a function that returns the JWT. The client automatically sends this with every API call in the Authorization: Bearer header.

supabase.auth.getClaims() only works with Supabase-issued JWTs

The supabase.auth.getClaims() method is meant only for JWTs issued by Supabase Auth. If you mint your own JWTs using an imported signing key, verification may fail. For self-minted or third-party JWTs, use a JWT verification library for your language instead.

JWTs provide foundation for Row Level Security

JWTs provide the foundation for Row Level Security. Each Supabase product can securely decode and verify JWT validity before using Postgres policies and roles to authorize access to the project's data.

Verify JWT with shared secret (HS256) signing key

If using a shared secret (HS256) signing key, verify by sending a request to GET https://project-id.supabase.co/auth/v1/user with the JWT in the Authorization: Bearer header and the publishable key in the apikey header. HTTP 200 OK means the JWT is valid. Avoid this check from edge servers or functions due to latency—prefer servers in the same geographical region as the project.

Pitfall: shared secret JWTs create security vulnerabilities

Using a shared secret (HS256) signing key creates significant security risks: it makes SOC2, PCI-DSS, ISO27000, HIPAA compliance harder; a malicious actor with the secret can impersonate users and access privileged actions; it's difficult to detect compromise; the secret might be accessible to multiple people and systems; a compromised secret can be used far into the future; secrets are easily leaked in source code, environment variables, or app packages; rotating shared secrets requires careful coordination.

Recommendation: use asymmetric keys instead of shared secrets

Strongly prefer using asymmetric signing keys based on public key cryptography (RSA or Elliptic Curves) instead of shared secret (HS256) keys. If you must verify an HS256 JWT, rely on the Auth server endpoint rather than verifying with the shared secret directly.

TypeScript example: verify JWT with jose library

import { createRemoteJWKSet, jwtVerify } from 'jose' const PROJECT_JWKS = createRemoteJWKSet( new URL('https://project-id.supabase.co/auth/v1/.well-known/jwks.json') ) async function verifyProjectJWT(jwt: string) { return jwtVerify(jwt, PROJECT_JWKS) }

JWKS caching and key rotation timing

The JWKS endpoint is cached by Supabase Edge for 10 minutes. Do not cache this data for longer than 10 minutes in your application, as it makes revocation difficult. Wait at least 20 minutes when creating a standby signing key or revoking a previously used key to account for cache expiry.

Supabase JWKS endpoint

Supabase Auth exposes a JSON Web Key Set at GET https://project-id.supabase.co/auth/v1/.well-known/jwks.json containing asymmetric JWT signing keys (public keys only). This endpoint returns an object with a keys array containing key objects with kid, alg, kty (RSA, EC, or OKP), and key_ops (["verify"]) fields. This endpoint is cached by Supabase Edge for 10 minutes.

TypeScript example: custom JWT with Supabase client

import { createClient } from '@supabase/supabase-js' const supabase = createClient( 'https://<supabase-project>.supabase.co', 'SUPABASE_PUBLISHABLE_KEY', { accessToken: async () => { return '<your JWT here>' }, } )

OAuth access tokens include user_id, role, and client_id claims

Access tokens issued by Supabase Auth OAuth are standard Supabase JWTs that include `user_id`, `role`, and `client_id` claims. Existing Row Level Security policies automatically apply to OAuth tokens, providing fine-grained control over what each client can access.

Custom Access Token Hooks work with OAuth tokens

Custom Access Token Hooks are triggered for all token issuance and can inject custom claims based on the OAuth client. Use client_id or authentication_method (oauth_provider/authorization_code for OAuth flows) to differentiate OAuth from regular authentication. This is useful for customizing standard JWT claims like audience (aud) or adding client-specific metadata.

Custom access token hook: Customize audience claim for OAuth clients

A common use case for Custom Access Token Hooks is customizing the audience claim for different OAuth clients. This allows third-party services to validate that tokens were issued specifically for them. The hook receives user, claims, and client_id, and returns custom claims as JSON. Example: for mobile-app-client-id return aud: 'https://api.myapp.com' and app_version: '2.0.0'; for analytics-partner-id return aud: 'https://analytics.partner.com' and access_level: 'read-only'.

Use asymmetric JWT signing for OAuth

For OAuth use cases, Supabase recommends migrating from HS256 (symmetric) to asymmetric algorithms like RS256 or ES256. Asymmetric keys are more scalable and secure because OAuth clients can validate JWTs using the public key from the JWKS endpoint without needing to share the JWT secret with third-party applications, and it creates a more resilient architecture for distributed systems. If using OpenID Connect ID tokens (by requesting the openid scope), asymmetric signing algorithms are required; ID token generation will fail with HS256.

Access token JWT claims for OAuth clients

OAuth access tokens are JWTs containing standard Supabase claims plus client_id claim. Standard claims include: aud ('authenticated'), exp (expiration timestamp), iat (issued-at timestamp), iss (issuer URL), sub (user UUID), email, phone, app_metadata (containing provider and providers array), user_metadata, role ('authenticated'), aal (authentication assurance level), amr (array of authentication methods with method and timestamp), session_id. The client_id claim indicates which OAuth client obtained the token.

JWT token validation with JWKS endpoint

Third-party clients should validate access tokens using the JWKS endpoint at https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json which contains public keys for token verification. Always verify: (1) Signature - token is signed by Supabase Auth, (2) Issuer (iss) - matches project URL, (3) Audience (aud) - is 'authenticated', (4) Expiration (exp) - token not expired, (5) Client ID (client_id) - matches your client if applicable.

Give your agent this brain