@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.
- 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,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
|
+
}
|