11import { browser , dev } from '$app/environment' ;
2+ import { env as dynamicEnv } from '$env/dynamic/private' ;
23import * as env from '$env/static/private' ;
3- import type { HasuraToken , MaybeToken , Rule } from '$lib/types/oidc' ;
4+ import type { MaybeToken , Rule } from '$lib/types/oidc' ;
45import { type Cookies , type RequestEvent } from '@sveltejs/kit' ;
56import * as arctic from 'arctic' ;
67import crypto from 'crypto' ;
@@ -27,6 +28,68 @@ const DEFAULT_JWKS_CLIENT = (() => {
2728// Can be overridden via OIDC_ALGORITHMS env var (space-separated, e.g., "RS256 RS384 RS512")
2829const SUPPORTED_ALGORITHMS = ( env . OIDC_ALGORITHMS ?. split ( ' ' ) || [ 'RS256' ] ) as jwt . Algorithm [ ] ;
2930
31+ /**
32+ * JWT claim path configuration.
33+ * These paths specify where to find user identity and role information in the JWT.
34+ *
35+ * Default paths follow Hasura's JWT claims namespace convention:
36+ * https://hasura.io/jwt/claims -> x-hasura-user-id, x-hasura-allowed-roles, x-hasura-default-role
37+ *
38+ * For custom IdP configurations, override with environment variables:
39+ * OIDC_CLAIMS_NAMESPACE: The top-level claim key (default: "https://hasura.io/jwt/claims")
40+ * OIDC_CLAIMS_USER_ID: The user ID claim within the namespace (default: "x-hasura-user-id")
41+ * OIDC_CLAIMS_ALLOWED_ROLES: The allowed roles claim (default: "x-hasura-allowed-roles")
42+ * OIDC_CLAIMS_DEFAULT_ROLE: The default role claim (default: "x-hasura-default-role")
43+ *
44+ * IMPORTANT: These must match the JWT configuration in:
45+ * - Hasura's HASURA_GRAPHQL_JWT_SECRET claims_map
46+ * - Aerie Gateway's JWT parsing logic
47+ * - Your IdP's token mapper configuration
48+ */
49+ export const CLAIMS_CONFIG = {
50+ namespace : dynamicEnv . OIDC_CLAIMS_NAMESPACE || 'https://hasura.io/jwt/claims' ,
51+ userId : dynamicEnv . OIDC_CLAIMS_USER_ID || 'x-hasura-user-id' ,
52+ allowedRoles : dynamicEnv . OIDC_CLAIMS_ALLOWED_ROLES || 'x-hasura-allowed-roles' ,
53+ defaultRole : dynamicEnv . OIDC_CLAIMS_DEFAULT_ROLE || 'x-hasura-default-role' ,
54+ } ;
55+
56+ /**
57+ * Extract claims from a decoded JWT token using the configured claim paths.
58+ * Supports nested claims via the namespace configuration.
59+ *
60+ * @param token - The decoded JWT payload
61+ * @returns Object with userId, allowedRoles, and defaultRole
62+ * @throws Error if required claims are missing
63+ */
64+ export function extractClaims ( token : jwt . JwtPayload ) : {
65+ userId : string ;
66+ allowedRoles : string [ ] ;
67+ defaultRole : string ;
68+ } {
69+ const namespace = token [ CLAIMS_CONFIG . namespace ] ;
70+ if ( ! namespace || typeof namespace !== 'object' ) {
71+ throw new Error ( `JWT missing claims namespace: ${ CLAIMS_CONFIG . namespace } ` ) ;
72+ }
73+
74+ const userId = namespace [ CLAIMS_CONFIG . userId ] ;
75+ const allowedRoles = namespace [ CLAIMS_CONFIG . allowedRoles ] ;
76+ const defaultRole = namespace [ CLAIMS_CONFIG . defaultRole ] ;
77+
78+ if ( ! userId || typeof userId !== 'string' ) {
79+ throw new Error ( `JWT missing or invalid user ID claim: ${ CLAIMS_CONFIG . namespace } .${ CLAIMS_CONFIG . userId } ` ) ;
80+ }
81+ if ( ! Array . isArray ( allowedRoles ) ) {
82+ throw new Error (
83+ `JWT missing or invalid allowed roles claim: ${ CLAIMS_CONFIG . namespace } .${ CLAIMS_CONFIG . allowedRoles } ` ,
84+ ) ;
85+ }
86+ if ( ! defaultRole || typeof defaultRole !== 'string' ) {
87+ throw new Error ( `JWT missing or invalid default role claim: ${ CLAIMS_CONFIG . namespace } .${ CLAIMS_CONFIG . defaultRole } ` ) ;
88+ }
89+
90+ return { userId, allowedRoles, defaultRole } ;
91+ }
92+
3093/**
3194 * Base verification options for all tokens (signature, issuer, expiration).
3295 * Access tokens are treated as opaque by OIDC clients - audience validation
@@ -320,30 +383,25 @@ const mutation = `mutation InsertUser($input: users_insert_input!) {
320383 }
321384}` ; // TODO: update other user tables in permissions schema?
322385
323- async function upsertUser ( decodedAccessToken : HasuraToken , accessToken : string ) : Promise < void > {
324- const username = decodedAccessToken [ 'https://hasura.io/jwt/claims' ] [ 'x-hasura-user-id' ] ;
325- // const defaultRole = decodedAccessToken['https://hasura.io/jwt/ claims']['x-hasura-default-role'] ;
326- const allowedRoles = decodedAccessToken [ 'https://hasura.io/jwt/ claims' ] [ 'x-hasura-allowed-roles' ] ;
386+ async function upsertUser ( decodedAccessToken : jwt . JwtPayload , accessToken : string ) : Promise < void > {
387+ const claims = extractClaims ( decodedAccessToken ) ;
388+ const username = claims . userId ;
389+ const allowedRoles = claims . allowedRoles ;
327390
328- // set the active and default role manually:
391+ // Set the active and default role based on priority (aerie_admin > user > viewer)
329392 let defaultRole = 'viewer' ;
330- switch ( true ) {
331- case allowedRoles . includes ( 'aerie_admin' ) :
332- defaultRole = 'aerie_admin' ;
333- break ;
334- case allowedRoles . includes ( 'user' ) :
335- defaultRole = 'user' ;
336- break ;
337- default :
338- defaultRole = 'viewer' ;
393+ if ( allowedRoles . includes ( 'aerie_admin' ) ) {
394+ defaultRole = 'aerie_admin' ;
395+ } else if ( allowedRoles . includes ( 'user' ) ) {
396+ defaultRole = 'user' ;
339397 }
340398
341399 const input = { default_role : defaultRole , username } ;
342400 const user : User = {
343- activeRole : defaultRole , // TODO: check allowed roles and pick highest. forget about default role.
401+ activeRole : defaultRole ,
344402 allowedRoles,
345403 defaultRole,
346- id : username , // TODO: not exactly. I think this is supposed to be decodedAccessToken.sub. but we don't even use it.
404+ id : username ,
347405 permissibleQueries : null ,
348406 rolePermissions : null ,
349407 token : accessToken ,
@@ -375,7 +433,7 @@ export async function updateWithNewTokens(cookies: Cookies, tokens: arctic.OAuth
375433
376434 // sort of an edge case, but if default role does change at the idp, it wouldn't hurt to update the local entry
377435 // TODO: should this be here? Where else could it go?
378- await upsertUser ( accessJwt as HasuraToken , tokens . accessToken ( ) ) ;
436+ await upsertUser ( accessJwt as jwt . JwtPayload , tokens . accessToken ( ) ) ;
379437 return true ;
380438 }
381439
0 commit comments