diff --git a/packages/core/auth-js/src/lib/types.ts b/packages/core/auth-js/src/lib/types.ts index 5a2327d6..930d994f 100644 --- a/packages/core/auth-js/src/lib/types.ts +++ b/packages/core/auth-js/src/lib/types.ts @@ -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 } diff --git a/packages/core/auth-js/test/GoTrueClient.test.ts b/packages/core/auth-js/test/GoTrueClient.test.ts index 5ed3fd4f..d5141511 100644 --- a/packages/core/auth-js/test/GoTrueClient.test.ts +++ b/packages/core/auth-js/test/GoTrueClient.test.ts @@ -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[] = []