Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion packages/core/auth-js/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1439,7 +1439,31 @@ export type RequiredClaims = {
session_id: string
}

export type JwtPayload = RequiredClaims & {
/**
* JWT Payload containing claims for Supabase authentication tokens.
*
* Required claims (iss, aud, exp, iat, sub, role, aal, session_id) are inherited from RequiredClaims.
* All other claims are optional as they can be customized via Custom Access Token Hooks.
*
* @see https://supabase.com/docs/guides/auth/jwt-fields
*/
export interface JwtPayload extends RequiredClaims {
// Standard optional claims (can be customized via custom access token hooks)
email?: string
phone?: string
is_anonymous?: boolean

// Optional claims
jti?: string
nbf?: number
app_metadata?: UserAppMetadata
user_metadata?: UserMetadata
amr?: AMREntry[]

// Special claims (only in anon/service role tokens)
ref?: string

// Allow custom claims via custom access token hooks
[key: string]: any
}

Expand Down
81 changes: 81 additions & 0 deletions packages/core/auth-js/test/GoTrueClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1613,6 +1613,87 @@ describe('getClaims', () => {
expect(authWithSession.getUser).toHaveBeenCalled()
})

test('getClaims returns properly typed JwtPayload with documented fields', async () => {
const { email, password } = mockUserCredentials()
const {
data: { user },
error: initialError,
} = await authWithSession.signUp({
email,
password,
})
expect(initialError).toBeNull()
expect(user).not.toBeNull()

const { data, error } = await authWithSession.getClaims()
expect(error).toBeNull()
expect(data).not.toBeNull()

const claims = data?.claims
expect(claims).toBeDefined()

// Test core required claims that are always present
expect(typeof claims?.sub).toBe('string')
expect(typeof claims?.role).toBe('string')

// Test standard optional claims
if (claims?.email !== undefined) {
expect(typeof claims.email).toBe('string')
}
if (claims?.phone !== undefined) {
expect(typeof claims.phone).toBe('string')
}
if (claims?.user_metadata !== undefined) {
expect(typeof claims.user_metadata).toBe('object')
}
if (claims?.app_metadata !== undefined) {
expect(typeof claims.app_metadata).toBe('object')
}
if (claims?.is_anonymous !== undefined) {
expect(typeof claims.is_anonymous).toBe('boolean')
}

// Test optional JWT standard claims if present
if (claims?.iss !== undefined) {
expect(typeof claims.iss).toBe('string')
}
if (claims?.aud !== undefined) {
expect(['string', 'object']).toContain(typeof claims.aud)
}
if (claims?.exp !== undefined) {
expect(typeof claims.exp).toBe('number')
}
if (claims?.iat !== undefined) {
expect(typeof claims.iat).toBe('number')
}
if (claims?.aal !== undefined) {
expect(typeof claims.aal).toBe('string')
}
if (claims?.session_id !== undefined) {
expect(typeof claims.session_id).toBe('string')
}
if (claims?.jti !== undefined) {
expect(typeof claims.jti).toBe('string')
}
if (claims?.nbf !== undefined) {
expect(typeof claims.nbf).toBe('number')
}

// Verify amr array structure if present
if (claims?.amr) {
expect(Array.isArray(claims.amr)).toBe(true)
if (claims.amr.length > 0) {
expect(typeof claims.amr[0].method).toBe('string')
expect(typeof claims.amr[0].timestamp).toBe('number')
}
}

// Verify ref claim if present (anon/service role tokens)
if (claims?.ref !== undefined) {
expect(typeof claims.ref).toBe('string')
}
})

test('getClaims fetches JWKS to verify asymmetric jwt', async () => {
const fetchedUrls: any[] = []
const fetchedResponse: any[] = []
Expand Down