import OidcProfile from "../schemas/OidcProfile"; /** * Map OIDC claims to normalized profile * * Extracts standard OIDC claims from the ID token and UserInfo response * and maps them to a consistent profile structure. * * Standard OIDC claims reference: * https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims * * @param claims - OIDC claims from ID token and/or UserInfo endpoint * @returns Normalized user profile */ export function mapClaimsToProfile(claims: Record): OidcProfile { return { // Required - subject identifier (unique user ID) id: claims.sub, // Email email: claims.email, emailVerified: claims.email_verified, // Name fields name: claims.name, givenName: claims.given_name, familyName: claims.family_name, middleName: claims.middle_name, // Username username: claims.preferred_username || claims.username, // Picture picture: claims.picture, // Phone phoneNumber: claims.phone_number, phoneNumberVerified: claims.phone_number_verified, // Keep all raw claims for custom access raw: claims, }; } /** * Extract custom claims using claim mapping configuration * * Allows extracting custom claims from the ID token using dot notation. * Supports nested claim paths. * * @param claims - OIDC claims from ID token * @param claimMapping - Map of field names to claim paths * @returns Object with extracted custom claims * * Example: * ```typescript * const claims = { * sub: '123', * email: 'user@example.com', * 'custom:department': 'Engineering', * 'custom:role': 'Admin', * groups: ['engineers', 'admins'] * }; * * const mapping = { * department: 'custom:department', * role: 'custom:role', * groups: 'groups' * }; * * const custom = extractCustomClaims(claims, mapping); * // { department: 'Engineering', role: 'Admin', groups: ['engineers', 'admins'] } * ``` */ export function extractCustomClaims(claims: Record, claimMapping: Record): Record { const customClaims: Record = {}; for (const [fieldName, claimPath] of Object.entries(claimMapping)) { const value = getClaimByPath(claims, claimPath); if (value !== undefined) { customClaims[fieldName] = value; } } return customClaims; } /** * Get claim value by path (supports dot notation) * * @param claims - Claims object * @param path - Claim path (e.g., "custom:department" or "address.street_address") * @returns Claim value or undefined if not found */ function getClaimByPath(claims: Record, path: string): any { // Handle direct access first (for paths with colons like "custom:department") if (path in claims) { return claims[path]; } // Handle nested paths with dot notation const parts = path.split("."); let value: any = claims; for (const part of parts) { if (value && typeof value === "object" && part in value) { value = value[part]; } else { return undefined; } } return value; }