@flink-app/oidc-plugin 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +846 -0
  4. package/dist/OidcInternalContext.d.ts +15 -0
  5. package/dist/OidcInternalContext.d.ts.map +1 -0
  6. package/dist/OidcInternalContext.js +2 -0
  7. package/dist/OidcPlugin.d.ts +77 -0
  8. package/dist/OidcPlugin.d.ts.map +1 -0
  9. package/dist/OidcPlugin.js +274 -0
  10. package/dist/OidcPluginContext.d.ts +73 -0
  11. package/dist/OidcPluginContext.d.ts.map +1 -0
  12. package/dist/OidcPluginContext.js +2 -0
  13. package/dist/OidcPluginOptions.d.ts +267 -0
  14. package/dist/OidcPluginOptions.d.ts.map +1 -0
  15. package/dist/OidcPluginOptions.js +2 -0
  16. package/dist/OidcProviderConfig.d.ts +77 -0
  17. package/dist/OidcProviderConfig.d.ts.map +1 -0
  18. package/dist/OidcProviderConfig.js +2 -0
  19. package/dist/handlers/CallbackOidc.d.ts +38 -0
  20. package/dist/handlers/CallbackOidc.d.ts.map +1 -0
  21. package/dist/handlers/CallbackOidc.js +219 -0
  22. package/dist/handlers/InitiateOidc.d.ts +35 -0
  23. package/dist/handlers/InitiateOidc.d.ts.map +1 -0
  24. package/dist/handlers/InitiateOidc.js +91 -0
  25. package/dist/index.d.ts +27 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +40 -0
  28. package/dist/providers/OidcProvider.d.ts +90 -0
  29. package/dist/providers/OidcProvider.d.ts.map +1 -0
  30. package/dist/providers/OidcProvider.js +208 -0
  31. package/dist/providers/ProviderRegistry.d.ts +55 -0
  32. package/dist/providers/ProviderRegistry.d.ts.map +1 -0
  33. package/dist/providers/ProviderRegistry.js +94 -0
  34. package/dist/repos/OidcConnectionRepo.d.ts +75 -0
  35. package/dist/repos/OidcConnectionRepo.d.ts.map +1 -0
  36. package/dist/repos/OidcConnectionRepo.js +122 -0
  37. package/dist/repos/OidcSessionRepo.d.ts +57 -0
  38. package/dist/repos/OidcSessionRepo.d.ts.map +1 -0
  39. package/dist/repos/OidcSessionRepo.js +91 -0
  40. package/dist/schemas/CallbackRequest.d.ts +37 -0
  41. package/dist/schemas/CallbackRequest.d.ts.map +1 -0
  42. package/dist/schemas/CallbackRequest.js +2 -0
  43. package/dist/schemas/InitiateRequest.d.ts +17 -0
  44. package/dist/schemas/InitiateRequest.d.ts.map +1 -0
  45. package/dist/schemas/InitiateRequest.js +2 -0
  46. package/dist/schemas/OidcConnection.d.ts +69 -0
  47. package/dist/schemas/OidcConnection.d.ts.map +1 -0
  48. package/dist/schemas/OidcConnection.js +2 -0
  49. package/dist/schemas/OidcProfile.d.ts +69 -0
  50. package/dist/schemas/OidcProfile.d.ts.map +1 -0
  51. package/dist/schemas/OidcProfile.js +2 -0
  52. package/dist/schemas/OidcSession.d.ts +46 -0
  53. package/dist/schemas/OidcSession.d.ts.map +1 -0
  54. package/dist/schemas/OidcSession.js +2 -0
  55. package/dist/schemas/OidcTokenSet.d.ts +42 -0
  56. package/dist/schemas/OidcTokenSet.d.ts.map +1 -0
  57. package/dist/schemas/OidcTokenSet.js +2 -0
  58. package/dist/utils/claims-mapper.d.ts +46 -0
  59. package/dist/utils/claims-mapper.d.ts.map +1 -0
  60. package/dist/utils/claims-mapper.js +104 -0
  61. package/dist/utils/encryption-utils.d.ts +32 -0
  62. package/dist/utils/encryption-utils.d.ts.map +1 -0
  63. package/dist/utils/encryption-utils.js +82 -0
  64. package/dist/utils/error-utils.d.ts +65 -0
  65. package/dist/utils/error-utils.d.ts.map +1 -0
  66. package/dist/utils/error-utils.js +150 -0
  67. package/dist/utils/response-utils.d.ts +18 -0
  68. package/dist/utils/response-utils.d.ts.map +1 -0
  69. package/dist/utils/response-utils.js +42 -0
  70. package/dist/utils/state-utils.d.ts +36 -0
  71. package/dist/utils/state-utils.d.ts.map +1 -0
  72. package/dist/utils/state-utils.js +66 -0
  73. package/examples/basic-oidc.ts +151 -0
  74. package/examples/multi-provider.ts +146 -0
  75. package/package.json +44 -0
  76. package/spec/handlers/InitiateOidc.spec.ts +62 -0
  77. package/spec/helpers/reporter.ts +34 -0
  78. package/spec/helpers/test-helpers.ts +108 -0
  79. package/spec/plugin/OidcPlugin.spec.ts +126 -0
  80. package/spec/providers/ProviderRegistry.spec.ts +197 -0
  81. package/spec/repos/OidcConnectionRepo.spec.ts +257 -0
  82. package/spec/repos/OidcSessionRepo.spec.ts +196 -0
  83. package/spec/support/jasmine.json +7 -0
  84. package/spec/utils/claims-mapper.spec.ts +257 -0
  85. package/spec/utils/encryption-utils.spec.ts +126 -0
  86. package/spec/utils/error-utils.spec.ts +107 -0
  87. package/spec/utils/state-utils.spec.ts +102 -0
  88. package/src/OidcInternalContext.ts +15 -0
  89. package/src/OidcPlugin.ts +290 -0
  90. package/src/OidcPluginContext.ts +76 -0
  91. package/src/OidcPluginOptions.ts +286 -0
  92. package/src/OidcProviderConfig.ts +87 -0
  93. package/src/handlers/CallbackOidc.ts +257 -0
  94. package/src/handlers/InitiateOidc.ts +110 -0
  95. package/src/index.ts +38 -0
  96. package/src/providers/OidcProvider.ts +237 -0
  97. package/src/providers/ProviderRegistry.ts +107 -0
  98. package/src/repos/OidcConnectionRepo.ts +132 -0
  99. package/src/repos/OidcSessionRepo.ts +99 -0
  100. package/src/schemas/CallbackRequest.ts +41 -0
  101. package/src/schemas/InitiateRequest.ts +17 -0
  102. package/src/schemas/OidcConnection.ts +80 -0
  103. package/src/schemas/OidcProfile.ts +79 -0
  104. package/src/schemas/OidcSession.ts +52 -0
  105. package/src/schemas/OidcTokenSet.ts +47 -0
  106. package/src/utils/claims-mapper.ts +114 -0
  107. package/src/utils/encryption-utils.ts +92 -0
  108. package/src/utils/error-utils.ts +167 -0
  109. package/src/utils/response-utils.ts +41 -0
  110. package/src/utils/state-utils.ts +66 -0
  111. package/tsconfig.dist.json +9 -0
  112. package/tsconfig.json +20 -0
@@ -0,0 +1,80 @@
1
+ /**
2
+ * OIDC connection linking a user to an IdP
3
+ *
4
+ * Persistent record of the user's connection to an OIDC provider.
5
+ * Stores the mapping between the app's user and the IdP's subject identifier.
6
+ * Optionally stores encrypted OAuth tokens if storeTokens is enabled.
7
+ */
8
+ export default interface OidcConnection {
9
+ /**
10
+ * MongoDB document ID
11
+ */
12
+ _id?: string;
13
+
14
+ /**
15
+ * Application user ID
16
+ * References the user in your app's user collection
17
+ */
18
+ userId: string;
19
+
20
+ /**
21
+ * OIDC provider name (e.g., "acme", "contoso")
22
+ */
23
+ provider: string;
24
+
25
+ /**
26
+ * OIDC subject identifier from the IdP
27
+ * The 'sub' claim from the ID token - unique per user per IdP
28
+ */
29
+ subject: string;
30
+
31
+ /**
32
+ * OIDC issuer identifier
33
+ * The 'iss' claim from the ID token - identifies the IdP
34
+ */
35
+ issuer: string;
36
+
37
+ /**
38
+ * User's email from the IdP
39
+ * Optional - for reference and display
40
+ */
41
+ email?: string;
42
+
43
+ /**
44
+ * Encrypted access token (if storeTokens enabled)
45
+ * Used to call IdP APIs on behalf of the user
46
+ */
47
+ accessToken?: string;
48
+
49
+ /**
50
+ * Encrypted refresh token (if storeTokens enabled)
51
+ * Used to obtain new access tokens
52
+ */
53
+ refreshToken?: string;
54
+
55
+ /**
56
+ * Encrypted ID token (if storeTokens enabled)
57
+ * The JWT containing user claims
58
+ */
59
+ idToken?: string;
60
+
61
+ /**
62
+ * Space-separated list of granted scopes
63
+ */
64
+ scope?: string;
65
+
66
+ /**
67
+ * Access token expiration time
68
+ */
69
+ expiresAt?: Date;
70
+
71
+ /**
72
+ * Connection creation timestamp
73
+ */
74
+ createdAt: Date;
75
+
76
+ /**
77
+ * Last update timestamp
78
+ */
79
+ updatedAt: Date;
80
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Normalized user profile from OIDC ID token and UserInfo endpoint
3
+ *
4
+ * This is the standardized profile format passed to the onAuthSuccess callback.
5
+ * Maps OIDC standard claims to a consistent profile structure.
6
+ */
7
+ export default interface OidcProfile {
8
+ /**
9
+ * Subject identifier - unique user ID from the IdP
10
+ * OIDC standard claim: 'sub'
11
+ */
12
+ id: string;
13
+
14
+ /**
15
+ * User's email address
16
+ * OIDC standard claim: 'email'
17
+ */
18
+ email: string;
19
+
20
+ /**
21
+ * Whether the email has been verified by the IdP
22
+ * OIDC standard claim: 'email_verified'
23
+ */
24
+ emailVerified?: boolean;
25
+
26
+ /**
27
+ * User's full name
28
+ * OIDC standard claim: 'name'
29
+ */
30
+ name?: string;
31
+
32
+ /**
33
+ * User's given name (first name)
34
+ * OIDC standard claim: 'given_name'
35
+ */
36
+ givenName?: string;
37
+
38
+ /**
39
+ * User's family name (last name)
40
+ * OIDC standard claim: 'family_name'
41
+ */
42
+ familyName?: string;
43
+
44
+ /**
45
+ * User's middle name
46
+ * OIDC standard claim: 'middle_name'
47
+ */
48
+ middleName?: string;
49
+
50
+ /**
51
+ * User's preferred username
52
+ * OIDC standard claim: 'preferred_username'
53
+ */
54
+ username?: string;
55
+
56
+ /**
57
+ * URL of the user's profile picture
58
+ * OIDC standard claim: 'picture'
59
+ */
60
+ picture?: string;
61
+
62
+ /**
63
+ * User's phone number
64
+ * OIDC standard claim: 'phone_number'
65
+ */
66
+ phoneNumber?: string;
67
+
68
+ /**
69
+ * Whether the phone number has been verified
70
+ * OIDC standard claim: 'phone_number_verified'
71
+ */
72
+ phoneNumberVerified?: boolean;
73
+
74
+ /**
75
+ * Raw OIDC claims from ID token and UserInfo
76
+ * Contains all claims returned by the IdP
77
+ */
78
+ raw: Record<string, any>;
79
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * OIDC session stored during the authorization flow
3
+ *
4
+ * Temporary session that exists only during the OAuth/OIDC flow (typically 10 minutes).
5
+ * Used for CSRF protection (state), PKCE (codeVerifier), and replay protection (nonce).
6
+ */
7
+ export default interface OidcSession {
8
+ /**
9
+ * MongoDB document ID
10
+ */
11
+ _id?: string;
12
+
13
+ /**
14
+ * Unique session identifier
15
+ */
16
+ sessionId: string;
17
+
18
+ /**
19
+ * CSRF protection token
20
+ * Random value used to prevent cross-site request forgery attacks
21
+ */
22
+ state: string;
23
+
24
+ /**
25
+ * PKCE code verifier
26
+ * Secret value used to prove the client initiated the authorization request
27
+ */
28
+ codeVerifier: string;
29
+
30
+ /**
31
+ * Nonce for ID token validation
32
+ * Random value used to prevent replay attacks on the ID token
33
+ */
34
+ nonce: string;
35
+
36
+ /**
37
+ * Provider name (e.g., "acme", "contoso")
38
+ */
39
+ provider: string;
40
+
41
+ /**
42
+ * URL to redirect to after successful authentication
43
+ * Can be overridden by the client via query parameter
44
+ */
45
+ redirectUri: string;
46
+
47
+ /**
48
+ * Session creation timestamp
49
+ * MongoDB TTL index will automatically delete expired sessions
50
+ */
51
+ createdAt: Date;
52
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * OIDC token set returned from the token endpoint
3
+ *
4
+ * Contains the tokens issued by the IdP after successful authorization.
5
+ */
6
+ export default interface OidcTokenSet {
7
+ /**
8
+ * Access token for calling IdP APIs
9
+ * Used to access protected resources at the IdP
10
+ */
11
+ accessToken: string;
12
+
13
+ /**
14
+ * ID token (JWT) containing user claims
15
+ * This is the core OIDC token that contains user identity information
16
+ */
17
+ idToken: string;
18
+
19
+ /**
20
+ * Refresh token for obtaining new access tokens
21
+ * Optional - only if IdP supports and grants refresh tokens
22
+ */
23
+ refreshToken?: string;
24
+
25
+ /**
26
+ * Token type (usually "Bearer")
27
+ */
28
+ tokenType: string;
29
+
30
+ /**
31
+ * Expiration time in seconds
32
+ * How many seconds until the access token expires
33
+ */
34
+ expiresIn?: number;
35
+
36
+ /**
37
+ * Scope granted by the IdP
38
+ * Space-separated list of scopes
39
+ */
40
+ scope?: string;
41
+
42
+ /**
43
+ * All claims from the ID token
44
+ * Parsed and validated JWT claims
45
+ */
46
+ claims: Record<string, any>;
47
+ }
@@ -0,0 +1,114 @@
1
+ import OidcProfile from "../schemas/OidcProfile";
2
+
3
+ /**
4
+ * Map OIDC claims to normalized profile
5
+ *
6
+ * Extracts standard OIDC claims from the ID token and UserInfo response
7
+ * and maps them to a consistent profile structure.
8
+ *
9
+ * Standard OIDC claims reference:
10
+ * https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
11
+ *
12
+ * @param claims - OIDC claims from ID token and/or UserInfo endpoint
13
+ * @returns Normalized user profile
14
+ */
15
+ export function mapClaimsToProfile(claims: Record<string, any>): OidcProfile {
16
+ return {
17
+ // Required - subject identifier (unique user ID)
18
+ id: claims.sub,
19
+
20
+ // Email
21
+ email: claims.email,
22
+ emailVerified: claims.email_verified,
23
+
24
+ // Name fields
25
+ name: claims.name,
26
+ givenName: claims.given_name,
27
+ familyName: claims.family_name,
28
+ middleName: claims.middle_name,
29
+
30
+ // Username
31
+ username: claims.preferred_username || claims.username,
32
+
33
+ // Picture
34
+ picture: claims.picture,
35
+
36
+ // Phone
37
+ phoneNumber: claims.phone_number,
38
+ phoneNumberVerified: claims.phone_number_verified,
39
+
40
+ // Keep all raw claims for custom access
41
+ raw: claims,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Extract custom claims using claim mapping configuration
47
+ *
48
+ * Allows extracting custom claims from the ID token using dot notation.
49
+ * Supports nested claim paths.
50
+ *
51
+ * @param claims - OIDC claims from ID token
52
+ * @param claimMapping - Map of field names to claim paths
53
+ * @returns Object with extracted custom claims
54
+ *
55
+ * Example:
56
+ * ```typescript
57
+ * const claims = {
58
+ * sub: '123',
59
+ * email: 'user@example.com',
60
+ * 'custom:department': 'Engineering',
61
+ * 'custom:role': 'Admin',
62
+ * groups: ['engineers', 'admins']
63
+ * };
64
+ *
65
+ * const mapping = {
66
+ * department: 'custom:department',
67
+ * role: 'custom:role',
68
+ * groups: 'groups'
69
+ * };
70
+ *
71
+ * const custom = extractCustomClaims(claims, mapping);
72
+ * // { department: 'Engineering', role: 'Admin', groups: ['engineers', 'admins'] }
73
+ * ```
74
+ */
75
+ export function extractCustomClaims(claims: Record<string, any>, claimMapping: Record<string, string>): Record<string, any> {
76
+ const customClaims: Record<string, any> = {};
77
+
78
+ for (const [fieldName, claimPath] of Object.entries(claimMapping)) {
79
+ const value = getClaimByPath(claims, claimPath);
80
+ if (value !== undefined) {
81
+ customClaims[fieldName] = value;
82
+ }
83
+ }
84
+
85
+ return customClaims;
86
+ }
87
+
88
+ /**
89
+ * Get claim value by path (supports dot notation)
90
+ *
91
+ * @param claims - Claims object
92
+ * @param path - Claim path (e.g., "custom:department" or "address.street_address")
93
+ * @returns Claim value or undefined if not found
94
+ */
95
+ function getClaimByPath(claims: Record<string, any>, path: string): any {
96
+ // Handle direct access first (for paths with colons like "custom:department")
97
+ if (path in claims) {
98
+ return claims[path];
99
+ }
100
+
101
+ // Handle nested paths with dot notation
102
+ const parts = path.split(".");
103
+ let value: any = claims;
104
+
105
+ for (const part of parts) {
106
+ if (value && typeof value === "object" && part in value) {
107
+ value = value[part];
108
+ } else {
109
+ return undefined;
110
+ }
111
+ }
112
+
113
+ return value;
114
+ }
@@ -0,0 +1,92 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes, createHash } from "crypto";
2
+
3
+ const ALGORITHM = "aes-256-gcm";
4
+ const IV_LENGTH = 16;
5
+ const AUTH_TAG_LENGTH = 16;
6
+ const SALT_LENGTH = 32;
7
+
8
+ /**
9
+ * Derive a 32-byte encryption key from a secret
10
+ *
11
+ * Uses SHA-256 to create a consistent key length from any secret.
12
+ *
13
+ * @param secret - Secret string (e.g., client secret)
14
+ * @returns 32-byte key suitable for AES-256
15
+ */
16
+ function deriveKey(secret: string): Buffer {
17
+ return createHash("sha256").update(secret).digest();
18
+ }
19
+
20
+ /**
21
+ * Validate encryption secret meets minimum requirements
22
+ *
23
+ * @param secret - Secret to validate
24
+ * @throws Error if secret is invalid
25
+ */
26
+ export function validateEncryptionSecret(secret: string): void {
27
+ if (!secret) {
28
+ throw new Error("Encryption secret is required");
29
+ }
30
+
31
+ if (secret.length < 32) {
32
+ throw new Error("Encryption secret must be at least 32 characters");
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Encrypt a token using AES-256-GCM
38
+ *
39
+ * Uses Galois/Counter Mode (GCM) which provides both confidentiality
40
+ * and authenticity (prevents tampering).
41
+ *
42
+ * Format: iv:authTag:encryptedData (all hex-encoded)
43
+ *
44
+ * @param token - Plain text token to encrypt
45
+ * @param secret - Encryption secret (at least 32 characters)
46
+ * @returns Encrypted token with IV and auth tag
47
+ */
48
+ export function encryptToken(token: string, secret: string): string {
49
+ const key = deriveKey(secret);
50
+ const iv = randomBytes(IV_LENGTH);
51
+ const cipher = createCipheriv(ALGORITHM, key, iv);
52
+
53
+ let encrypted = cipher.update(token, "utf8", "hex");
54
+ encrypted += cipher.final("hex");
55
+
56
+ const authTag = cipher.getAuthTag();
57
+
58
+ // Format: iv:authTag:encryptedData
59
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
60
+ }
61
+
62
+ /**
63
+ * Decrypt a token using AES-256-GCM
64
+ *
65
+ * Validates the auth tag to ensure the data hasn't been tampered with.
66
+ *
67
+ * @param encryptedToken - Encrypted token from encryptToken()
68
+ * @param secret - Encryption secret used during encryption
69
+ * @returns Decrypted plain text token
70
+ * @throws Error if decryption fails or auth tag is invalid
71
+ */
72
+ export function decryptToken(encryptedToken: string, secret: string): string {
73
+ const parts = encryptedToken.split(":");
74
+
75
+ if (parts.length !== 3) {
76
+ throw new Error("Invalid encrypted token format");
77
+ }
78
+
79
+ const [ivHex, authTagHex, encryptedData] = parts;
80
+
81
+ const key = deriveKey(secret);
82
+ const iv = Buffer.from(ivHex, "hex");
83
+ const authTag = Buffer.from(authTagHex, "hex");
84
+
85
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
86
+ decipher.setAuthTag(authTag);
87
+
88
+ let decrypted = decipher.update(encryptedData, "hex", "utf8");
89
+ decrypted += decipher.final("utf8");
90
+
91
+ return decrypted;
92
+ }
@@ -0,0 +1,167 @@
1
+ import { OidcError } from "../OidcPluginOptions";
2
+
3
+ /**
4
+ * Standard OIDC error codes
5
+ */
6
+ export const OidcErrorCodes = {
7
+ // Configuration errors
8
+ INVALID_PROVIDER: "invalid_provider",
9
+ PROVIDER_NOT_CONFIGURED: "provider_not_configured",
10
+ DISCOVERY_FAILED: "discovery_failed",
11
+
12
+ // Request validation errors
13
+ MISSING_CODE: "missing_code",
14
+ MISSING_STATE: "missing_state",
15
+ INVALID_STATE: "invalid_state",
16
+ INVALID_RESPONSE_TYPE: "invalid_response_type",
17
+
18
+ // Session errors
19
+ SESSION_NOT_FOUND: "session_not_found",
20
+ SESSION_EXPIRED: "session_expired",
21
+
22
+ // Token exchange errors
23
+ TOKEN_EXCHANGE_FAILED: "token_exchange_failed",
24
+ INVALID_TOKEN: "invalid_token",
25
+ ID_TOKEN_VALIDATION_FAILED: "id_token_validation_failed",
26
+
27
+ // User/Profile errors
28
+ USERINFO_FAILED: "userinfo_failed",
29
+ PROFILE_EXTRACTION_FAILED: "profile_extraction_failed",
30
+
31
+ // JWT generation errors
32
+ JWT_GENERATION_FAILED: "jwt_generation_failed",
33
+
34
+ // IdP errors (from authorization endpoint)
35
+ ACCESS_DENIED: "access_denied",
36
+ UNAUTHORIZED_CLIENT: "unauthorized_client",
37
+ INVALID_REQUEST: "invalid_request",
38
+ UNSUPPORTED_RESPONSE_TYPE: "unsupported_response_type",
39
+ INVALID_SCOPE: "invalid_scope",
40
+ SERVER_ERROR: "server_error",
41
+ TEMPORARILY_UNAVAILABLE: "temporarily_unavailable",
42
+ } as const;
43
+
44
+ /**
45
+ * Create a standardized OIDC error object
46
+ *
47
+ * @param code - Error code from OidcErrorCodes
48
+ * @param message - Human-readable error message
49
+ * @param details - Additional error details for debugging
50
+ * @returns OIDC error object
51
+ */
52
+ export function createOidcError(code: string, message: string, details?: any): OidcError {
53
+ const error: OidcError = {
54
+ code,
55
+ message,
56
+ };
57
+
58
+ if (details) {
59
+ error.details = details;
60
+ }
61
+
62
+ // Make it throwable
63
+ const throwableError = new Error(message) as Error & OidcError;
64
+ throwableError.code = code;
65
+ throwableError.details = details;
66
+
67
+ return throwableError as any;
68
+ }
69
+
70
+ /**
71
+ * Validate provider name format
72
+ *
73
+ * Provider names must be alphanumeric with optional hyphens/underscores
74
+ * to ensure they work correctly in URLs.
75
+ *
76
+ * @param provider - Provider name to validate
77
+ * @throws OidcError if invalid
78
+ */
79
+ export function validateProvider(provider: string): void {
80
+ if (!provider) {
81
+ throw createOidcError(OidcErrorCodes.INVALID_PROVIDER, "Provider name is required", { provider });
82
+ }
83
+
84
+ // Allow alphanumeric, hyphens, underscores
85
+ if (!/^[a-zA-Z0-9_-]+$/.test(provider)) {
86
+ throw createOidcError(OidcErrorCodes.INVALID_PROVIDER, "Provider name must be alphanumeric (hyphens and underscores allowed)", { provider });
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Validate response type parameter
92
+ *
93
+ * @param responseType - Response type from query parameter
94
+ * @throws OidcError if invalid
95
+ */
96
+ export function validateResponseType(responseType?: string): void {
97
+ if (responseType && responseType !== "json") {
98
+ throw createOidcError(OidcErrorCodes.INVALID_RESPONSE_TYPE, 'Invalid response_type. Must be "json" or omitted for redirect', {
99
+ responseType,
100
+ });
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Map IdP error codes to user-friendly messages
106
+ *
107
+ * Maps OAuth 2.0 / OIDC error codes from the IdP to our standardized
108
+ * error format with helpful messages.
109
+ *
110
+ * @param error - Error object or string from IdP
111
+ * @returns Standardized OIDC error
112
+ */
113
+ export function handleProviderError(error: any): OidcError {
114
+ const errorCode = typeof error === "string" ? error : error.error || error.code;
115
+ const errorDescription = error.error_description || error.message;
116
+
117
+ // Map common OAuth/OIDC errors
118
+ switch (errorCode) {
119
+ case "access_denied":
120
+ return createOidcError(OidcErrorCodes.ACCESS_DENIED, "User denied authorization", {
121
+ originalError: errorCode,
122
+ description: errorDescription,
123
+ });
124
+
125
+ case "unauthorized_client":
126
+ return createOidcError(OidcErrorCodes.UNAUTHORIZED_CLIENT, "Client not authorized for this request", {
127
+ originalError: errorCode,
128
+ description: errorDescription,
129
+ });
130
+
131
+ case "invalid_request":
132
+ return createOidcError(OidcErrorCodes.INVALID_REQUEST, "Invalid authorization request", {
133
+ originalError: errorCode,
134
+ description: errorDescription,
135
+ });
136
+
137
+ case "unsupported_response_type":
138
+ return createOidcError(OidcErrorCodes.UNSUPPORTED_RESPONSE_TYPE, "Response type not supported by IdP", {
139
+ originalError: errorCode,
140
+ description: errorDescription,
141
+ });
142
+
143
+ case "invalid_scope":
144
+ return createOidcError(OidcErrorCodes.INVALID_SCOPE, "Invalid or unsupported scope", {
145
+ originalError: errorCode,
146
+ description: errorDescription,
147
+ });
148
+
149
+ case "server_error":
150
+ return createOidcError(OidcErrorCodes.SERVER_ERROR, "IdP server error", {
151
+ originalError: errorCode,
152
+ description: errorDescription,
153
+ });
154
+
155
+ case "temporarily_unavailable":
156
+ return createOidcError(OidcErrorCodes.TEMPORARILY_UNAVAILABLE, "IdP temporarily unavailable", {
157
+ originalError: errorCode,
158
+ description: errorDescription,
159
+ });
160
+
161
+ default:
162
+ return createOidcError(OidcErrorCodes.SERVER_ERROR, errorDescription || "Unknown IdP error", {
163
+ originalError: errorCode,
164
+ description: errorDescription,
165
+ });
166
+ }
167
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Format token response for the client
3
+ *
4
+ * Supports two response formats:
5
+ * 1. JSON response (for API clients)
6
+ * 2. Redirect with token in URL fragment (for web browsers)
7
+ *
8
+ * URL fragments are used for security - they are NOT sent to the server
9
+ * in HTTP requests and are only accessible to client-side JavaScript.
10
+ *
11
+ * @param token - JWT token for the application
12
+ * @param user - User object
13
+ * @param redirectUrl - URL to redirect to
14
+ * @param responseType - "json" or undefined (redirect)
15
+ * @returns Flink response object
16
+ */
17
+ export function formatTokenResponse(token: string, user: any, redirectUrl: string, responseType?: "json"): any {
18
+ // JSON response for API clients
19
+ if (responseType === "json") {
20
+ return {
21
+ data: {
22
+ user,
23
+ token,
24
+ },
25
+ };
26
+ }
27
+
28
+ // Redirect response for web browsers
29
+ // Token is in URL fragment (#token=...) for security
30
+ const separator = redirectUrl.includes("#") ? "&" : "#";
31
+ const tokenFragment = `token=${encodeURIComponent(token)}`;
32
+ const finalUrl = `${redirectUrl}${separator}${tokenFragment}`;
33
+
34
+ return {
35
+ status: 302,
36
+ headers: {
37
+ Location: finalUrl,
38
+ },
39
+ data: {},
40
+ };
41
+ }