@flink-app/oidc-plugin 1.0.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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +846 -0
- package/dist/OidcInternalContext.d.ts +15 -0
- package/dist/OidcInternalContext.d.ts.map +1 -0
- package/dist/OidcInternalContext.js +2 -0
- package/dist/OidcPlugin.d.ts +77 -0
- package/dist/OidcPlugin.d.ts.map +1 -0
- package/dist/OidcPlugin.js +274 -0
- package/dist/OidcPluginContext.d.ts +73 -0
- package/dist/OidcPluginContext.d.ts.map +1 -0
- package/dist/OidcPluginContext.js +2 -0
- package/dist/OidcPluginOptions.d.ts +267 -0
- package/dist/OidcPluginOptions.d.ts.map +1 -0
- package/dist/OidcPluginOptions.js +2 -0
- package/dist/OidcProviderConfig.d.ts +77 -0
- package/dist/OidcProviderConfig.d.ts.map +1 -0
- package/dist/OidcProviderConfig.js +2 -0
- package/dist/handlers/CallbackOidc.d.ts +38 -0
- package/dist/handlers/CallbackOidc.d.ts.map +1 -0
- package/dist/handlers/CallbackOidc.js +219 -0
- package/dist/handlers/InitiateOidc.d.ts +35 -0
- package/dist/handlers/InitiateOidc.d.ts.map +1 -0
- package/dist/handlers/InitiateOidc.js +91 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/providers/OidcProvider.d.ts +90 -0
- package/dist/providers/OidcProvider.d.ts.map +1 -0
- package/dist/providers/OidcProvider.js +208 -0
- package/dist/providers/ProviderRegistry.d.ts +55 -0
- package/dist/providers/ProviderRegistry.d.ts.map +1 -0
- package/dist/providers/ProviderRegistry.js +94 -0
- package/dist/repos/OidcConnectionRepo.d.ts +75 -0
- package/dist/repos/OidcConnectionRepo.d.ts.map +1 -0
- package/dist/repos/OidcConnectionRepo.js +122 -0
- package/dist/repos/OidcSessionRepo.d.ts +57 -0
- package/dist/repos/OidcSessionRepo.d.ts.map +1 -0
- package/dist/repos/OidcSessionRepo.js +91 -0
- package/dist/schemas/CallbackRequest.d.ts +37 -0
- package/dist/schemas/CallbackRequest.d.ts.map +1 -0
- package/dist/schemas/CallbackRequest.js +2 -0
- package/dist/schemas/InitiateRequest.d.ts +17 -0
- package/dist/schemas/InitiateRequest.d.ts.map +1 -0
- package/dist/schemas/InitiateRequest.js +2 -0
- package/dist/schemas/OidcConnection.d.ts +69 -0
- package/dist/schemas/OidcConnection.d.ts.map +1 -0
- package/dist/schemas/OidcConnection.js +2 -0
- package/dist/schemas/OidcProfile.d.ts +69 -0
- package/dist/schemas/OidcProfile.d.ts.map +1 -0
- package/dist/schemas/OidcProfile.js +2 -0
- package/dist/schemas/OidcSession.d.ts +46 -0
- package/dist/schemas/OidcSession.d.ts.map +1 -0
- package/dist/schemas/OidcSession.js +2 -0
- package/dist/schemas/OidcTokenSet.d.ts +42 -0
- package/dist/schemas/OidcTokenSet.d.ts.map +1 -0
- package/dist/schemas/OidcTokenSet.js +2 -0
- package/dist/utils/claims-mapper.d.ts +46 -0
- package/dist/utils/claims-mapper.d.ts.map +1 -0
- package/dist/utils/claims-mapper.js +104 -0
- package/dist/utils/encryption-utils.d.ts +32 -0
- package/dist/utils/encryption-utils.d.ts.map +1 -0
- package/dist/utils/encryption-utils.js +82 -0
- package/dist/utils/error-utils.d.ts +65 -0
- package/dist/utils/error-utils.d.ts.map +1 -0
- package/dist/utils/error-utils.js +150 -0
- package/dist/utils/response-utils.d.ts +18 -0
- package/dist/utils/response-utils.d.ts.map +1 -0
- package/dist/utils/response-utils.js +42 -0
- package/dist/utils/state-utils.d.ts +36 -0
- package/dist/utils/state-utils.d.ts.map +1 -0
- package/dist/utils/state-utils.js +66 -0
- package/examples/basic-oidc.ts +151 -0
- package/examples/multi-provider.ts +146 -0
- package/package.json +44 -0
- package/spec/handlers/InitiateOidc.spec.ts +62 -0
- package/spec/helpers/reporter.ts +34 -0
- package/spec/helpers/test-helpers.ts +108 -0
- package/spec/plugin/OidcPlugin.spec.ts +126 -0
- package/spec/providers/ProviderRegistry.spec.ts +197 -0
- package/spec/repos/OidcConnectionRepo.spec.ts +257 -0
- package/spec/repos/OidcSessionRepo.spec.ts +196 -0
- package/spec/support/jasmine.json +7 -0
- package/spec/utils/claims-mapper.spec.ts +257 -0
- package/spec/utils/encryption-utils.spec.ts +126 -0
- package/spec/utils/error-utils.spec.ts +107 -0
- package/spec/utils/state-utils.spec.ts +102 -0
- package/src/OidcInternalContext.ts +15 -0
- package/src/OidcPlugin.ts +290 -0
- package/src/OidcPluginContext.ts +76 -0
- package/src/OidcPluginOptions.ts +286 -0
- package/src/OidcProviderConfig.ts +87 -0
- package/src/handlers/CallbackOidc.ts +257 -0
- package/src/handlers/InitiateOidc.ts +110 -0
- package/src/index.ts +38 -0
- package/src/providers/OidcProvider.ts +237 -0
- package/src/providers/ProviderRegistry.ts +107 -0
- package/src/repos/OidcConnectionRepo.ts +132 -0
- package/src/repos/OidcSessionRepo.ts +99 -0
- package/src/schemas/CallbackRequest.ts +41 -0
- package/src/schemas/InitiateRequest.ts +17 -0
- package/src/schemas/OidcConnection.ts +80 -0
- package/src/schemas/OidcProfile.ts +79 -0
- package/src/schemas/OidcSession.ts +52 -0
- package/src/schemas/OidcTokenSet.ts +47 -0
- package/src/utils/claims-mapper.ts +114 -0
- package/src/utils/encryption-utils.ts +92 -0
- package/src/utils/error-utils.ts +167 -0
- package/src/utils/response-utils.ts +41 -0
- package/src/utils/state-utils.ts +66 -0
- package/tsconfig.dist.json +9 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import OidcProfile from "../schemas/OidcProfile";
|
|
2
|
+
/**
|
|
3
|
+
* Map OIDC claims to normalized profile
|
|
4
|
+
*
|
|
5
|
+
* Extracts standard OIDC claims from the ID token and UserInfo response
|
|
6
|
+
* and maps them to a consistent profile structure.
|
|
7
|
+
*
|
|
8
|
+
* Standard OIDC claims reference:
|
|
9
|
+
* https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
|
10
|
+
*
|
|
11
|
+
* @param claims - OIDC claims from ID token and/or UserInfo endpoint
|
|
12
|
+
* @returns Normalized user profile
|
|
13
|
+
*/
|
|
14
|
+
export declare function mapClaimsToProfile(claims: Record<string, any>): OidcProfile;
|
|
15
|
+
/**
|
|
16
|
+
* Extract custom claims using claim mapping configuration
|
|
17
|
+
*
|
|
18
|
+
* Allows extracting custom claims from the ID token using dot notation.
|
|
19
|
+
* Supports nested claim paths.
|
|
20
|
+
*
|
|
21
|
+
* @param claims - OIDC claims from ID token
|
|
22
|
+
* @param claimMapping - Map of field names to claim paths
|
|
23
|
+
* @returns Object with extracted custom claims
|
|
24
|
+
*
|
|
25
|
+
* Example:
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const claims = {
|
|
28
|
+
* sub: '123',
|
|
29
|
+
* email: 'user@example.com',
|
|
30
|
+
* 'custom:department': 'Engineering',
|
|
31
|
+
* 'custom:role': 'Admin',
|
|
32
|
+
* groups: ['engineers', 'admins']
|
|
33
|
+
* };
|
|
34
|
+
*
|
|
35
|
+
* const mapping = {
|
|
36
|
+
* department: 'custom:department',
|
|
37
|
+
* role: 'custom:role',
|
|
38
|
+
* groups: 'groups'
|
|
39
|
+
* };
|
|
40
|
+
*
|
|
41
|
+
* const custom = extractCustomClaims(claims, mapping);
|
|
42
|
+
* // { department: 'Engineering', role: 'Admin', groups: ['engineers', 'admins'] }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function extractCustomClaims(claims: Record<string, any>, claimMapping: Record<string, string>): Record<string, any>;
|
|
46
|
+
//# sourceMappingURL=claims-mapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claims-mapper.d.ts","sourceRoot":"","sources":["../../src/utils/claims-mapper.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,wBAAwB,CAAC;AAEjD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,WAAW,CA4B3E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAW1H"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mapClaimsToProfile = mapClaimsToProfile;
|
|
4
|
+
exports.extractCustomClaims = extractCustomClaims;
|
|
5
|
+
/**
|
|
6
|
+
* Map OIDC claims to normalized profile
|
|
7
|
+
*
|
|
8
|
+
* Extracts standard OIDC claims from the ID token and UserInfo response
|
|
9
|
+
* and maps them to a consistent profile structure.
|
|
10
|
+
*
|
|
11
|
+
* Standard OIDC claims reference:
|
|
12
|
+
* https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
|
13
|
+
*
|
|
14
|
+
* @param claims - OIDC claims from ID token and/or UserInfo endpoint
|
|
15
|
+
* @returns Normalized user profile
|
|
16
|
+
*/
|
|
17
|
+
function mapClaimsToProfile(claims) {
|
|
18
|
+
return {
|
|
19
|
+
// Required - subject identifier (unique user ID)
|
|
20
|
+
id: claims.sub,
|
|
21
|
+
// Email
|
|
22
|
+
email: claims.email,
|
|
23
|
+
emailVerified: claims.email_verified,
|
|
24
|
+
// Name fields
|
|
25
|
+
name: claims.name,
|
|
26
|
+
givenName: claims.given_name,
|
|
27
|
+
familyName: claims.family_name,
|
|
28
|
+
middleName: claims.middle_name,
|
|
29
|
+
// Username
|
|
30
|
+
username: claims.preferred_username || claims.username,
|
|
31
|
+
// Picture
|
|
32
|
+
picture: claims.picture,
|
|
33
|
+
// Phone
|
|
34
|
+
phoneNumber: claims.phone_number,
|
|
35
|
+
phoneNumberVerified: claims.phone_number_verified,
|
|
36
|
+
// Keep all raw claims for custom access
|
|
37
|
+
raw: claims,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Extract custom claims using claim mapping configuration
|
|
42
|
+
*
|
|
43
|
+
* Allows extracting custom claims from the ID token using dot notation.
|
|
44
|
+
* Supports nested claim paths.
|
|
45
|
+
*
|
|
46
|
+
* @param claims - OIDC claims from ID token
|
|
47
|
+
* @param claimMapping - Map of field names to claim paths
|
|
48
|
+
* @returns Object with extracted custom claims
|
|
49
|
+
*
|
|
50
|
+
* Example:
|
|
51
|
+
* ```typescript
|
|
52
|
+
* const claims = {
|
|
53
|
+
* sub: '123',
|
|
54
|
+
* email: 'user@example.com',
|
|
55
|
+
* 'custom:department': 'Engineering',
|
|
56
|
+
* 'custom:role': 'Admin',
|
|
57
|
+
* groups: ['engineers', 'admins']
|
|
58
|
+
* };
|
|
59
|
+
*
|
|
60
|
+
* const mapping = {
|
|
61
|
+
* department: 'custom:department',
|
|
62
|
+
* role: 'custom:role',
|
|
63
|
+
* groups: 'groups'
|
|
64
|
+
* };
|
|
65
|
+
*
|
|
66
|
+
* const custom = extractCustomClaims(claims, mapping);
|
|
67
|
+
* // { department: 'Engineering', role: 'Admin', groups: ['engineers', 'admins'] }
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
function extractCustomClaims(claims, claimMapping) {
|
|
71
|
+
const customClaims = {};
|
|
72
|
+
for (const [fieldName, claimPath] of Object.entries(claimMapping)) {
|
|
73
|
+
const value = getClaimByPath(claims, claimPath);
|
|
74
|
+
if (value !== undefined) {
|
|
75
|
+
customClaims[fieldName] = value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return customClaims;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get claim value by path (supports dot notation)
|
|
82
|
+
*
|
|
83
|
+
* @param claims - Claims object
|
|
84
|
+
* @param path - Claim path (e.g., "custom:department" or "address.street_address")
|
|
85
|
+
* @returns Claim value or undefined if not found
|
|
86
|
+
*/
|
|
87
|
+
function getClaimByPath(claims, path) {
|
|
88
|
+
// Handle direct access first (for paths with colons like "custom:department")
|
|
89
|
+
if (path in claims) {
|
|
90
|
+
return claims[path];
|
|
91
|
+
}
|
|
92
|
+
// Handle nested paths with dot notation
|
|
93
|
+
const parts = path.split(".");
|
|
94
|
+
let value = claims;
|
|
95
|
+
for (const part of parts) {
|
|
96
|
+
if (value && typeof value === "object" && part in value) {
|
|
97
|
+
value = value[part];
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate encryption secret meets minimum requirements
|
|
3
|
+
*
|
|
4
|
+
* @param secret - Secret to validate
|
|
5
|
+
* @throws Error if secret is invalid
|
|
6
|
+
*/
|
|
7
|
+
export declare function validateEncryptionSecret(secret: string): void;
|
|
8
|
+
/**
|
|
9
|
+
* Encrypt a token using AES-256-GCM
|
|
10
|
+
*
|
|
11
|
+
* Uses Galois/Counter Mode (GCM) which provides both confidentiality
|
|
12
|
+
* and authenticity (prevents tampering).
|
|
13
|
+
*
|
|
14
|
+
* Format: iv:authTag:encryptedData (all hex-encoded)
|
|
15
|
+
*
|
|
16
|
+
* @param token - Plain text token to encrypt
|
|
17
|
+
* @param secret - Encryption secret (at least 32 characters)
|
|
18
|
+
* @returns Encrypted token with IV and auth tag
|
|
19
|
+
*/
|
|
20
|
+
export declare function encryptToken(token: string, secret: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Decrypt a token using AES-256-GCM
|
|
23
|
+
*
|
|
24
|
+
* Validates the auth tag to ensure the data hasn't been tampered with.
|
|
25
|
+
*
|
|
26
|
+
* @param encryptedToken - Encrypted token from encryptToken()
|
|
27
|
+
* @param secret - Encryption secret used during encryption
|
|
28
|
+
* @returns Decrypted plain text token
|
|
29
|
+
* @throws Error if decryption fails or auth tag is invalid
|
|
30
|
+
*/
|
|
31
|
+
export declare function decryptToken(encryptedToken: string, secret: string): string;
|
|
32
|
+
//# sourceMappingURL=encryption-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encryption-utils.d.ts","sourceRoot":"","sources":["../../src/utils/encryption-utils.ts"],"names":[],"mappings":"AAmBA;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAQ7D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAYlE;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAoB3E"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateEncryptionSecret = validateEncryptionSecret;
|
|
4
|
+
exports.encryptToken = encryptToken;
|
|
5
|
+
exports.decryptToken = decryptToken;
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
const ALGORITHM = "aes-256-gcm";
|
|
8
|
+
const IV_LENGTH = 16;
|
|
9
|
+
const AUTH_TAG_LENGTH = 16;
|
|
10
|
+
const SALT_LENGTH = 32;
|
|
11
|
+
/**
|
|
12
|
+
* Derive a 32-byte encryption key from a secret
|
|
13
|
+
*
|
|
14
|
+
* Uses SHA-256 to create a consistent key length from any secret.
|
|
15
|
+
*
|
|
16
|
+
* @param secret - Secret string (e.g., client secret)
|
|
17
|
+
* @returns 32-byte key suitable for AES-256
|
|
18
|
+
*/
|
|
19
|
+
function deriveKey(secret) {
|
|
20
|
+
return (0, crypto_1.createHash)("sha256").update(secret).digest();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Validate encryption secret meets minimum requirements
|
|
24
|
+
*
|
|
25
|
+
* @param secret - Secret to validate
|
|
26
|
+
* @throws Error if secret is invalid
|
|
27
|
+
*/
|
|
28
|
+
function validateEncryptionSecret(secret) {
|
|
29
|
+
if (!secret) {
|
|
30
|
+
throw new Error("Encryption secret is required");
|
|
31
|
+
}
|
|
32
|
+
if (secret.length < 32) {
|
|
33
|
+
throw new Error("Encryption secret must be at least 32 characters");
|
|
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
|
+
function encryptToken(token, secret) {
|
|
49
|
+
const key = deriveKey(secret);
|
|
50
|
+
const iv = (0, crypto_1.randomBytes)(IV_LENGTH);
|
|
51
|
+
const cipher = (0, crypto_1.createCipheriv)(ALGORITHM, key, iv);
|
|
52
|
+
let encrypted = cipher.update(token, "utf8", "hex");
|
|
53
|
+
encrypted += cipher.final("hex");
|
|
54
|
+
const authTag = cipher.getAuthTag();
|
|
55
|
+
// Format: iv:authTag:encryptedData
|
|
56
|
+
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Decrypt a token using AES-256-GCM
|
|
60
|
+
*
|
|
61
|
+
* Validates the auth tag to ensure the data hasn't been tampered with.
|
|
62
|
+
*
|
|
63
|
+
* @param encryptedToken - Encrypted token from encryptToken()
|
|
64
|
+
* @param secret - Encryption secret used during encryption
|
|
65
|
+
* @returns Decrypted plain text token
|
|
66
|
+
* @throws Error if decryption fails or auth tag is invalid
|
|
67
|
+
*/
|
|
68
|
+
function decryptToken(encryptedToken, secret) {
|
|
69
|
+
const parts = encryptedToken.split(":");
|
|
70
|
+
if (parts.length !== 3) {
|
|
71
|
+
throw new Error("Invalid encrypted token format");
|
|
72
|
+
}
|
|
73
|
+
const [ivHex, authTagHex, encryptedData] = parts;
|
|
74
|
+
const key = deriveKey(secret);
|
|
75
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
76
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
77
|
+
const decipher = (0, crypto_1.createDecipheriv)(ALGORITHM, key, iv);
|
|
78
|
+
decipher.setAuthTag(authTag);
|
|
79
|
+
let decrypted = decipher.update(encryptedData, "hex", "utf8");
|
|
80
|
+
decrypted += decipher.final("utf8");
|
|
81
|
+
return decrypted;
|
|
82
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { OidcError } from "../OidcPluginOptions";
|
|
2
|
+
/**
|
|
3
|
+
* Standard OIDC error codes
|
|
4
|
+
*/
|
|
5
|
+
export declare const OidcErrorCodes: {
|
|
6
|
+
readonly INVALID_PROVIDER: "invalid_provider";
|
|
7
|
+
readonly PROVIDER_NOT_CONFIGURED: "provider_not_configured";
|
|
8
|
+
readonly DISCOVERY_FAILED: "discovery_failed";
|
|
9
|
+
readonly MISSING_CODE: "missing_code";
|
|
10
|
+
readonly MISSING_STATE: "missing_state";
|
|
11
|
+
readonly INVALID_STATE: "invalid_state";
|
|
12
|
+
readonly INVALID_RESPONSE_TYPE: "invalid_response_type";
|
|
13
|
+
readonly SESSION_NOT_FOUND: "session_not_found";
|
|
14
|
+
readonly SESSION_EXPIRED: "session_expired";
|
|
15
|
+
readonly TOKEN_EXCHANGE_FAILED: "token_exchange_failed";
|
|
16
|
+
readonly INVALID_TOKEN: "invalid_token";
|
|
17
|
+
readonly ID_TOKEN_VALIDATION_FAILED: "id_token_validation_failed";
|
|
18
|
+
readonly USERINFO_FAILED: "userinfo_failed";
|
|
19
|
+
readonly PROFILE_EXTRACTION_FAILED: "profile_extraction_failed";
|
|
20
|
+
readonly JWT_GENERATION_FAILED: "jwt_generation_failed";
|
|
21
|
+
readonly ACCESS_DENIED: "access_denied";
|
|
22
|
+
readonly UNAUTHORIZED_CLIENT: "unauthorized_client";
|
|
23
|
+
readonly INVALID_REQUEST: "invalid_request";
|
|
24
|
+
readonly UNSUPPORTED_RESPONSE_TYPE: "unsupported_response_type";
|
|
25
|
+
readonly INVALID_SCOPE: "invalid_scope";
|
|
26
|
+
readonly SERVER_ERROR: "server_error";
|
|
27
|
+
readonly TEMPORARILY_UNAVAILABLE: "temporarily_unavailable";
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Create a standardized OIDC error object
|
|
31
|
+
*
|
|
32
|
+
* @param code - Error code from OidcErrorCodes
|
|
33
|
+
* @param message - Human-readable error message
|
|
34
|
+
* @param details - Additional error details for debugging
|
|
35
|
+
* @returns OIDC error object
|
|
36
|
+
*/
|
|
37
|
+
export declare function createOidcError(code: string, message: string, details?: any): OidcError;
|
|
38
|
+
/**
|
|
39
|
+
* Validate provider name format
|
|
40
|
+
*
|
|
41
|
+
* Provider names must be alphanumeric with optional hyphens/underscores
|
|
42
|
+
* to ensure they work correctly in URLs.
|
|
43
|
+
*
|
|
44
|
+
* @param provider - Provider name to validate
|
|
45
|
+
* @throws OidcError if invalid
|
|
46
|
+
*/
|
|
47
|
+
export declare function validateProvider(provider: string): void;
|
|
48
|
+
/**
|
|
49
|
+
* Validate response type parameter
|
|
50
|
+
*
|
|
51
|
+
* @param responseType - Response type from query parameter
|
|
52
|
+
* @throws OidcError if invalid
|
|
53
|
+
*/
|
|
54
|
+
export declare function validateResponseType(responseType?: string): void;
|
|
55
|
+
/**
|
|
56
|
+
* Map IdP error codes to user-friendly messages
|
|
57
|
+
*
|
|
58
|
+
* Maps OAuth 2.0 / OIDC error codes from the IdP to our standardized
|
|
59
|
+
* error format with helpful messages.
|
|
60
|
+
*
|
|
61
|
+
* @param error - Error object or string from IdP
|
|
62
|
+
* @returns Standardized OIDC error
|
|
63
|
+
*/
|
|
64
|
+
export declare function handleProviderError(error: any): OidcError;
|
|
65
|
+
//# sourceMappingURL=error-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-utils.d.ts","sourceRoot":"","sources":["../../src/utils/error-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD;;GAEG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;CAoCjB,CAAC;AAEX;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,GAAG,SAAS,CAgBvF;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CASvD;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAMhE;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,GAAG,GAAG,SAAS,CAsDzD"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OidcErrorCodes = void 0;
|
|
4
|
+
exports.createOidcError = createOidcError;
|
|
5
|
+
exports.validateProvider = validateProvider;
|
|
6
|
+
exports.validateResponseType = validateResponseType;
|
|
7
|
+
exports.handleProviderError = handleProviderError;
|
|
8
|
+
/**
|
|
9
|
+
* Standard OIDC error codes
|
|
10
|
+
*/
|
|
11
|
+
exports.OidcErrorCodes = {
|
|
12
|
+
// Configuration errors
|
|
13
|
+
INVALID_PROVIDER: "invalid_provider",
|
|
14
|
+
PROVIDER_NOT_CONFIGURED: "provider_not_configured",
|
|
15
|
+
DISCOVERY_FAILED: "discovery_failed",
|
|
16
|
+
// Request validation errors
|
|
17
|
+
MISSING_CODE: "missing_code",
|
|
18
|
+
MISSING_STATE: "missing_state",
|
|
19
|
+
INVALID_STATE: "invalid_state",
|
|
20
|
+
INVALID_RESPONSE_TYPE: "invalid_response_type",
|
|
21
|
+
// Session errors
|
|
22
|
+
SESSION_NOT_FOUND: "session_not_found",
|
|
23
|
+
SESSION_EXPIRED: "session_expired",
|
|
24
|
+
// Token exchange errors
|
|
25
|
+
TOKEN_EXCHANGE_FAILED: "token_exchange_failed",
|
|
26
|
+
INVALID_TOKEN: "invalid_token",
|
|
27
|
+
ID_TOKEN_VALIDATION_FAILED: "id_token_validation_failed",
|
|
28
|
+
// User/Profile errors
|
|
29
|
+
USERINFO_FAILED: "userinfo_failed",
|
|
30
|
+
PROFILE_EXTRACTION_FAILED: "profile_extraction_failed",
|
|
31
|
+
// JWT generation errors
|
|
32
|
+
JWT_GENERATION_FAILED: "jwt_generation_failed",
|
|
33
|
+
// IdP errors (from authorization endpoint)
|
|
34
|
+
ACCESS_DENIED: "access_denied",
|
|
35
|
+
UNAUTHORIZED_CLIENT: "unauthorized_client",
|
|
36
|
+
INVALID_REQUEST: "invalid_request",
|
|
37
|
+
UNSUPPORTED_RESPONSE_TYPE: "unsupported_response_type",
|
|
38
|
+
INVALID_SCOPE: "invalid_scope",
|
|
39
|
+
SERVER_ERROR: "server_error",
|
|
40
|
+
TEMPORARILY_UNAVAILABLE: "temporarily_unavailable",
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Create a standardized OIDC error object
|
|
44
|
+
*
|
|
45
|
+
* @param code - Error code from OidcErrorCodes
|
|
46
|
+
* @param message - Human-readable error message
|
|
47
|
+
* @param details - Additional error details for debugging
|
|
48
|
+
* @returns OIDC error object
|
|
49
|
+
*/
|
|
50
|
+
function createOidcError(code, message, details) {
|
|
51
|
+
const error = {
|
|
52
|
+
code,
|
|
53
|
+
message,
|
|
54
|
+
};
|
|
55
|
+
if (details) {
|
|
56
|
+
error.details = details;
|
|
57
|
+
}
|
|
58
|
+
// Make it throwable
|
|
59
|
+
const throwableError = new Error(message);
|
|
60
|
+
throwableError.code = code;
|
|
61
|
+
throwableError.details = details;
|
|
62
|
+
return throwableError;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Validate provider name format
|
|
66
|
+
*
|
|
67
|
+
* Provider names must be alphanumeric with optional hyphens/underscores
|
|
68
|
+
* to ensure they work correctly in URLs.
|
|
69
|
+
*
|
|
70
|
+
* @param provider - Provider name to validate
|
|
71
|
+
* @throws OidcError if invalid
|
|
72
|
+
*/
|
|
73
|
+
function validateProvider(provider) {
|
|
74
|
+
if (!provider) {
|
|
75
|
+
throw createOidcError(exports.OidcErrorCodes.INVALID_PROVIDER, "Provider name is required", { provider });
|
|
76
|
+
}
|
|
77
|
+
// Allow alphanumeric, hyphens, underscores
|
|
78
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(provider)) {
|
|
79
|
+
throw createOidcError(exports.OidcErrorCodes.INVALID_PROVIDER, "Provider name must be alphanumeric (hyphens and underscores allowed)", { provider });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Validate response type parameter
|
|
84
|
+
*
|
|
85
|
+
* @param responseType - Response type from query parameter
|
|
86
|
+
* @throws OidcError if invalid
|
|
87
|
+
*/
|
|
88
|
+
function validateResponseType(responseType) {
|
|
89
|
+
if (responseType && responseType !== "json") {
|
|
90
|
+
throw createOidcError(exports.OidcErrorCodes.INVALID_RESPONSE_TYPE, 'Invalid response_type. Must be "json" or omitted for redirect', {
|
|
91
|
+
responseType,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Map IdP error codes to user-friendly messages
|
|
97
|
+
*
|
|
98
|
+
* Maps OAuth 2.0 / OIDC error codes from the IdP to our standardized
|
|
99
|
+
* error format with helpful messages.
|
|
100
|
+
*
|
|
101
|
+
* @param error - Error object or string from IdP
|
|
102
|
+
* @returns Standardized OIDC error
|
|
103
|
+
*/
|
|
104
|
+
function handleProviderError(error) {
|
|
105
|
+
const errorCode = typeof error === "string" ? error : error.error || error.code;
|
|
106
|
+
const errorDescription = error.error_description || error.message;
|
|
107
|
+
// Map common OAuth/OIDC errors
|
|
108
|
+
switch (errorCode) {
|
|
109
|
+
case "access_denied":
|
|
110
|
+
return createOidcError(exports.OidcErrorCodes.ACCESS_DENIED, "User denied authorization", {
|
|
111
|
+
originalError: errorCode,
|
|
112
|
+
description: errorDescription,
|
|
113
|
+
});
|
|
114
|
+
case "unauthorized_client":
|
|
115
|
+
return createOidcError(exports.OidcErrorCodes.UNAUTHORIZED_CLIENT, "Client not authorized for this request", {
|
|
116
|
+
originalError: errorCode,
|
|
117
|
+
description: errorDescription,
|
|
118
|
+
});
|
|
119
|
+
case "invalid_request":
|
|
120
|
+
return createOidcError(exports.OidcErrorCodes.INVALID_REQUEST, "Invalid authorization request", {
|
|
121
|
+
originalError: errorCode,
|
|
122
|
+
description: errorDescription,
|
|
123
|
+
});
|
|
124
|
+
case "unsupported_response_type":
|
|
125
|
+
return createOidcError(exports.OidcErrorCodes.UNSUPPORTED_RESPONSE_TYPE, "Response type not supported by IdP", {
|
|
126
|
+
originalError: errorCode,
|
|
127
|
+
description: errorDescription,
|
|
128
|
+
});
|
|
129
|
+
case "invalid_scope":
|
|
130
|
+
return createOidcError(exports.OidcErrorCodes.INVALID_SCOPE, "Invalid or unsupported scope", {
|
|
131
|
+
originalError: errorCode,
|
|
132
|
+
description: errorDescription,
|
|
133
|
+
});
|
|
134
|
+
case "server_error":
|
|
135
|
+
return createOidcError(exports.OidcErrorCodes.SERVER_ERROR, "IdP server error", {
|
|
136
|
+
originalError: errorCode,
|
|
137
|
+
description: errorDescription,
|
|
138
|
+
});
|
|
139
|
+
case "temporarily_unavailable":
|
|
140
|
+
return createOidcError(exports.OidcErrorCodes.TEMPORARILY_UNAVAILABLE, "IdP temporarily unavailable", {
|
|
141
|
+
originalError: errorCode,
|
|
142
|
+
description: errorDescription,
|
|
143
|
+
});
|
|
144
|
+
default:
|
|
145
|
+
return createOidcError(exports.OidcErrorCodes.SERVER_ERROR, errorDescription || "Unknown IdP error", {
|
|
146
|
+
originalError: errorCode,
|
|
147
|
+
description: errorDescription,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
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 declare function formatTokenResponse(token: string, user: any, redirectUrl: string, responseType?: "json"): any;
|
|
18
|
+
//# sourceMappingURL=response-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response-utils.d.ts","sourceRoot":"","sources":["../../src/utils/response-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,GAAG,CAwB7G"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatTokenResponse = formatTokenResponse;
|
|
4
|
+
/**
|
|
5
|
+
* Format token response for the client
|
|
6
|
+
*
|
|
7
|
+
* Supports two response formats:
|
|
8
|
+
* 1. JSON response (for API clients)
|
|
9
|
+
* 2. Redirect with token in URL fragment (for web browsers)
|
|
10
|
+
*
|
|
11
|
+
* URL fragments are used for security - they are NOT sent to the server
|
|
12
|
+
* in HTTP requests and are only accessible to client-side JavaScript.
|
|
13
|
+
*
|
|
14
|
+
* @param token - JWT token for the application
|
|
15
|
+
* @param user - User object
|
|
16
|
+
* @param redirectUrl - URL to redirect to
|
|
17
|
+
* @param responseType - "json" or undefined (redirect)
|
|
18
|
+
* @returns Flink response object
|
|
19
|
+
*/
|
|
20
|
+
function formatTokenResponse(token, user, redirectUrl, responseType) {
|
|
21
|
+
// JSON response for API clients
|
|
22
|
+
if (responseType === "json") {
|
|
23
|
+
return {
|
|
24
|
+
data: {
|
|
25
|
+
user,
|
|
26
|
+
token,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Redirect response for web browsers
|
|
31
|
+
// Token is in URL fragment (#token=...) for security
|
|
32
|
+
const separator = redirectUrl.includes("#") ? "&" : "#";
|
|
33
|
+
const tokenFragment = `token=${encodeURIComponent(token)}`;
|
|
34
|
+
const finalUrl = `${redirectUrl}${separator}${tokenFragment}`;
|
|
35
|
+
return {
|
|
36
|
+
status: 302,
|
|
37
|
+
headers: {
|
|
38
|
+
Location: finalUrl,
|
|
39
|
+
},
|
|
40
|
+
data: {},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate cryptographically secure state parameter for CSRF protection
|
|
3
|
+
*
|
|
4
|
+
* The state parameter is used to prevent CSRF attacks by ensuring the
|
|
5
|
+
* callback request originated from our initiate request.
|
|
6
|
+
*
|
|
7
|
+
* @returns Random 32-byte hex string
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateState(): string;
|
|
10
|
+
/**
|
|
11
|
+
* Generate cryptographically secure session ID
|
|
12
|
+
*
|
|
13
|
+
* @returns Random 16-byte hex string
|
|
14
|
+
*/
|
|
15
|
+
export declare function generateSessionId(): string;
|
|
16
|
+
/**
|
|
17
|
+
* Generate cryptographically secure nonce for ID token replay protection
|
|
18
|
+
*
|
|
19
|
+
* The nonce is included in the authorization request and must be present
|
|
20
|
+
* in the ID token claims to prevent replay attacks.
|
|
21
|
+
*
|
|
22
|
+
* @returns Random 16-byte hex string
|
|
23
|
+
*/
|
|
24
|
+
export declare function generateNonce(): string;
|
|
25
|
+
/**
|
|
26
|
+
* Validate state parameter using constant-time comparison
|
|
27
|
+
*
|
|
28
|
+
* Uses timing-safe comparison to prevent timing attacks that could
|
|
29
|
+
* reveal information about the expected state value.
|
|
30
|
+
*
|
|
31
|
+
* @param providedState - State from callback request
|
|
32
|
+
* @param expectedState - State from session
|
|
33
|
+
* @returns true if states match, false otherwise
|
|
34
|
+
*/
|
|
35
|
+
export declare function validateState(providedState: string, expectedState: string): boolean;
|
|
36
|
+
//# sourceMappingURL=state-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-utils.d.ts","sourceRoot":"","sources":["../../src/utils/state-utils.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAoBnF"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateState = generateState;
|
|
4
|
+
exports.generateSessionId = generateSessionId;
|
|
5
|
+
exports.generateNonce = generateNonce;
|
|
6
|
+
exports.validateState = validateState;
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
8
|
+
/**
|
|
9
|
+
* Generate cryptographically secure state parameter for CSRF protection
|
|
10
|
+
*
|
|
11
|
+
* The state parameter is used to prevent CSRF attacks by ensuring the
|
|
12
|
+
* callback request originated from our initiate request.
|
|
13
|
+
*
|
|
14
|
+
* @returns Random 32-byte hex string
|
|
15
|
+
*/
|
|
16
|
+
function generateState() {
|
|
17
|
+
return (0, crypto_1.randomBytes)(32).toString("hex");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Generate cryptographically secure session ID
|
|
21
|
+
*
|
|
22
|
+
* @returns Random 16-byte hex string
|
|
23
|
+
*/
|
|
24
|
+
function generateSessionId() {
|
|
25
|
+
return (0, crypto_1.randomBytes)(16).toString("hex");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Generate cryptographically secure nonce for ID token replay protection
|
|
29
|
+
*
|
|
30
|
+
* The nonce is included in the authorization request and must be present
|
|
31
|
+
* in the ID token claims to prevent replay attacks.
|
|
32
|
+
*
|
|
33
|
+
* @returns Random 16-byte hex string
|
|
34
|
+
*/
|
|
35
|
+
function generateNonce() {
|
|
36
|
+
return (0, crypto_1.randomBytes)(16).toString("hex");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Validate state parameter using constant-time comparison
|
|
40
|
+
*
|
|
41
|
+
* Uses timing-safe comparison to prevent timing attacks that could
|
|
42
|
+
* reveal information about the expected state value.
|
|
43
|
+
*
|
|
44
|
+
* @param providedState - State from callback request
|
|
45
|
+
* @param expectedState - State from session
|
|
46
|
+
* @returns true if states match, false otherwise
|
|
47
|
+
*/
|
|
48
|
+
function validateState(providedState, expectedState) {
|
|
49
|
+
if (!providedState || !expectedState) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
// Convert to buffers for constant-time comparison
|
|
53
|
+
const providedBuffer = Buffer.from(providedState, "utf8");
|
|
54
|
+
const expectedBuffer = Buffer.from(expectedState, "utf8");
|
|
55
|
+
// Buffers must be same length for timingSafeEqual
|
|
56
|
+
if (providedBuffer.length !== expectedBuffer.length) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
return (0, crypto_1.timingSafeEqual)(providedBuffer, expectedBuffer);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
// timingSafeEqual throws if lengths differ (shouldn't happen due to check above)
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|