@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,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for a single OIDC provider
|
|
3
|
+
*
|
|
4
|
+
* Supports both OIDC discovery and manual endpoint configuration.
|
|
5
|
+
*/
|
|
6
|
+
export interface OidcProviderConfig {
|
|
7
|
+
/**
|
|
8
|
+
* OIDC issuer URL
|
|
9
|
+
* e.g., "https://idp.acme.com" or "https://login.microsoftonline.com/{tenant}/v2.0"
|
|
10
|
+
* Used to validate the 'iss' claim in ID tokens
|
|
11
|
+
*/
|
|
12
|
+
issuer: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* OAuth 2.0 client ID
|
|
16
|
+
* Provided by the IdP when you register your application
|
|
17
|
+
*/
|
|
18
|
+
clientId: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* OAuth 2.0 client secret
|
|
22
|
+
* Provided by the IdP - keep this secure!
|
|
23
|
+
*/
|
|
24
|
+
clientSecret: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Callback URL for OAuth redirect
|
|
28
|
+
* Must match the redirect URI registered with the IdP
|
|
29
|
+
* e.g., "https://myapp.com/oidc/acme/callback"
|
|
30
|
+
*/
|
|
31
|
+
callbackUrl: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* OAuth 2.0 scopes to request
|
|
35
|
+
* Default: ["openid", "email", "profile"]
|
|
36
|
+
* "openid" is required for OIDC
|
|
37
|
+
*/
|
|
38
|
+
scope?: string[];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* OIDC discovery URL for automatic configuration
|
|
42
|
+
* e.g., "https://idp.acme.com/.well-known/openid-configuration"
|
|
43
|
+
* If provided, endpoints will be auto-discovered
|
|
44
|
+
* If not provided, must specify manual endpoints below
|
|
45
|
+
*/
|
|
46
|
+
discoveryUrl?: string;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Manual endpoint configuration (if not using discovery)
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Authorization endpoint URL
|
|
54
|
+
* e.g., "https://idp.acme.com/authorize"
|
|
55
|
+
* Required if discoveryUrl not provided
|
|
56
|
+
*/
|
|
57
|
+
authorizationEndpoint?: string;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Token endpoint URL
|
|
61
|
+
* e.g., "https://idp.acme.com/token"
|
|
62
|
+
* Required if discoveryUrl not provided
|
|
63
|
+
*/
|
|
64
|
+
tokenEndpoint?: string;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* UserInfo endpoint URL
|
|
68
|
+
* e.g., "https://idp.acme.com/userinfo"
|
|
69
|
+
* Optional - if not provided, only ID token claims will be used
|
|
70
|
+
*/
|
|
71
|
+
userinfoEndpoint?: string;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* JWKS (JSON Web Key Set) endpoint URL
|
|
75
|
+
* e.g., "https://idp.acme.com/.well-known/jwks.json"
|
|
76
|
+
* Required for ID token signature validation
|
|
77
|
+
* Required if discoveryUrl not provided
|
|
78
|
+
*/
|
|
79
|
+
jwksUri?: string;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Additional custom claims to extract from ID token
|
|
83
|
+
* Map of app field names to OIDC claim paths
|
|
84
|
+
* e.g., { "department": "custom:department", "role": "custom:role" }
|
|
85
|
+
*/
|
|
86
|
+
claimMapping?: Record<string, string>;
|
|
87
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OIDC Callback Handler
|
|
3
|
+
*
|
|
4
|
+
* Handles the OIDC callback from the provider by:
|
|
5
|
+
* 1. Validating the state parameter to prevent CSRF attacks
|
|
6
|
+
* 2. Exchanging the authorization code for tokens (with PKCE validation)
|
|
7
|
+
* 3. Validating the ID token (signature, claims, nonce)
|
|
8
|
+
* 4. Fetching user profile from ID token and UserInfo endpoint
|
|
9
|
+
* 5. Calling the onAuthSuccess callback to create/link user and generate JWT token
|
|
10
|
+
* 6. Optionally storing the OIDC connection (if storeTokens enabled)
|
|
11
|
+
* 7. Returning the JWT token to the client (via JSON or redirect)
|
|
12
|
+
*
|
|
13
|
+
* Route: GET /oidc/:provider/callback?code=...&state=...&response_type=json
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { GetHandler, HttpMethod, RouteProps, badRequest, internalServerError, log } from "@flink-app/flink";
|
|
17
|
+
import CallbackRequest from "../schemas/CallbackRequest";
|
|
18
|
+
import { validateState } from "../utils/state-utils";
|
|
19
|
+
import { formatTokenResponse } from "../utils/response-utils";
|
|
20
|
+
import { encryptToken } from "../utils/encryption-utils";
|
|
21
|
+
import { validateProvider, validateResponseType, createOidcError, OidcErrorCodes, handleProviderError } from "../utils/error-utils";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Path parameters for the handler
|
|
25
|
+
*/
|
|
26
|
+
interface PathParams {
|
|
27
|
+
provider: string;
|
|
28
|
+
[key: string]: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Route configuration
|
|
33
|
+
* This handler is registered programmatically by the plugin
|
|
34
|
+
*/
|
|
35
|
+
export const Route: RouteProps = {
|
|
36
|
+
path: "/oidc/:provider/callback",
|
|
37
|
+
method: HttpMethod.get,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* OIDC Callback Handler
|
|
42
|
+
*
|
|
43
|
+
* Completes the OIDC flow by exchanging the authorization code for tokens,
|
|
44
|
+
* validating the ID token, building user profile, calling the app's onAuthSuccess
|
|
45
|
+
* callback to generate JWT token, and returning the token to the client.
|
|
46
|
+
*/
|
|
47
|
+
const CallbackOidc: GetHandler<any, any, PathParams, CallbackRequest> = async ({ ctx, req }) => {
|
|
48
|
+
const { provider } = req.params;
|
|
49
|
+
const { code, state, error: oidcError, error_description, response_type } = req.query;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Validate provider and response_type
|
|
53
|
+
validateProvider(provider);
|
|
54
|
+
validateResponseType(response_type);
|
|
55
|
+
|
|
56
|
+
// Check for OIDC provider errors (e.g., user denied access)
|
|
57
|
+
if (oidcError) {
|
|
58
|
+
const error = handleProviderError({ error: oidcError, error_description });
|
|
59
|
+
|
|
60
|
+
// Call onAuthError callback if provided
|
|
61
|
+
const { options } = ctx.plugins.oidc;
|
|
62
|
+
if (options.onAuthError) {
|
|
63
|
+
const errorResult = await options.onAuthError({
|
|
64
|
+
error,
|
|
65
|
+
provider,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (errorResult.redirectUrl) {
|
|
69
|
+
return {
|
|
70
|
+
status: 302,
|
|
71
|
+
headers: { Location: errorResult.redirectUrl },
|
|
72
|
+
data: {},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return badRequest(error.message);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Validate required parameters
|
|
81
|
+
if (!code || !state) {
|
|
82
|
+
throw createOidcError(OidcErrorCodes.MISSING_CODE, "Missing authorization code or state parameter", {
|
|
83
|
+
hasCode: !!code,
|
|
84
|
+
hasState: !!state,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Find OIDC session by state
|
|
89
|
+
const session = await ctx.repos.oidcSessionRepo.getByState(state);
|
|
90
|
+
|
|
91
|
+
if (!session) {
|
|
92
|
+
throw createOidcError(OidcErrorCodes.SESSION_EXPIRED, "OIDC session not found or expired. Please try logging in again.", { state });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Validate state parameter (CSRF protection)
|
|
96
|
+
if (!validateState(state, session.state)) {
|
|
97
|
+
throw createOidcError(OidcErrorCodes.INVALID_STATE, "Invalid state parameter. Possible CSRF attack detected.", {
|
|
98
|
+
providedState: state.substring(0, 10) + "...",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Delete session immediately after validation (one-time use)
|
|
103
|
+
await ctx.repos.oidcSessionRepo.deleteBySessionId(session.sessionId);
|
|
104
|
+
|
|
105
|
+
// Get plugin options
|
|
106
|
+
const { options } = ctx.plugins.oidc;
|
|
107
|
+
|
|
108
|
+
// Get provider instance
|
|
109
|
+
const providerRegistry = (ctx as any).oidcProviderRegistry;
|
|
110
|
+
if (!providerRegistry) {
|
|
111
|
+
throw createOidcError(OidcErrorCodes.PROVIDER_NOT_CONFIGURED, "OIDC plugin not properly initialized");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const oidcProvider = await providerRegistry.getProvider(provider);
|
|
115
|
+
|
|
116
|
+
// Exchange authorization code for tokens with PKCE validation
|
|
117
|
+
const tokenSet = await oidcProvider.exchangeCodeForToken({
|
|
118
|
+
code,
|
|
119
|
+
codeVerifier: session.codeVerifier,
|
|
120
|
+
state: session.state,
|
|
121
|
+
nonce: session.nonce,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Build user profile from ID token and UserInfo
|
|
125
|
+
const profile = await oidcProvider.buildProfile(tokenSet, true);
|
|
126
|
+
|
|
127
|
+
// Call onAuthSuccess callback to create/link user and generate JWT token
|
|
128
|
+
const authSuccessParams = {
|
|
129
|
+
profile,
|
|
130
|
+
claims: tokenSet.claims,
|
|
131
|
+
provider,
|
|
132
|
+
...(options.storeTokens ? { tokens: tokenSet } : {}),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
let authResult;
|
|
136
|
+
try {
|
|
137
|
+
authResult = await options.onAuthSuccess(authSuccessParams, ctx);
|
|
138
|
+
} catch (error: any) {
|
|
139
|
+
// Handle JWT generation or user creation errors
|
|
140
|
+
log.error("OIDC onAuthSuccess callback failed:", error);
|
|
141
|
+
|
|
142
|
+
const oidcError = createOidcError(OidcErrorCodes.JWT_GENERATION_FAILED, "Failed to complete authentication. Please try again.", {
|
|
143
|
+
originalError: error.message,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Call onAuthError callback if provided
|
|
147
|
+
if (options.onAuthError) {
|
|
148
|
+
const errorResult = await options.onAuthError({
|
|
149
|
+
error: oidcError,
|
|
150
|
+
provider,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (errorResult.redirectUrl) {
|
|
154
|
+
return {
|
|
155
|
+
status: 302,
|
|
156
|
+
headers: { Location: errorResult.redirectUrl },
|
|
157
|
+
data: {},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return internalServerError("Authentication failed. Please try again.");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Extract user and JWT token from callback result
|
|
166
|
+
const { user, token, redirectUrl } = authResult;
|
|
167
|
+
|
|
168
|
+
if (!token) {
|
|
169
|
+
throw createOidcError(OidcErrorCodes.JWT_GENERATION_FAILED, "No authentication token returned from callback", { hasUser: !!user });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Store OIDC connection if token storage is enabled
|
|
173
|
+
if (options.storeTokens && user && user._id) {
|
|
174
|
+
// Get encryption secret (use provider client secret or global encryption key)
|
|
175
|
+
const staticProviderConfig = options.providers[provider];
|
|
176
|
+
const encryptionSecret = options.encryptionKey || staticProviderConfig?.clientSecret;
|
|
177
|
+
|
|
178
|
+
if (!encryptionSecret) {
|
|
179
|
+
log.warn("No encryption secret available, tokens will not be stored");
|
|
180
|
+
} else {
|
|
181
|
+
// Encrypt tokens before storing
|
|
182
|
+
const encryptedAccessToken = encryptToken(tokenSet.accessToken, encryptionSecret);
|
|
183
|
+
const encryptedIdToken = encryptToken(tokenSet.idToken, encryptionSecret);
|
|
184
|
+
const encryptedRefreshToken = tokenSet.refreshToken ? encryptToken(tokenSet.refreshToken, encryptionSecret) : undefined;
|
|
185
|
+
|
|
186
|
+
// Calculate token expiration
|
|
187
|
+
const expiresAt = tokenSet.expiresIn ? new Date(Date.now() + tokenSet.expiresIn * 1000) : undefined;
|
|
188
|
+
|
|
189
|
+
// Create or update OIDC connection
|
|
190
|
+
const existingConnection = await ctx.repos.oidcConnectionRepo.findByUserAndProvider(user._id, provider);
|
|
191
|
+
|
|
192
|
+
if (existingConnection) {
|
|
193
|
+
await ctx.repos.oidcConnectionRepo.updateOne(existingConnection._id!, {
|
|
194
|
+
accessToken: encryptedAccessToken,
|
|
195
|
+
idToken: encryptedIdToken,
|
|
196
|
+
refreshToken: encryptedRefreshToken,
|
|
197
|
+
scope: tokenSet.scope || "",
|
|
198
|
+
expiresAt,
|
|
199
|
+
updatedAt: new Date(),
|
|
200
|
+
});
|
|
201
|
+
} else {
|
|
202
|
+
await ctx.repos.oidcConnectionRepo.create({
|
|
203
|
+
userId: user._id,
|
|
204
|
+
provider,
|
|
205
|
+
subject: tokenSet.claims.sub,
|
|
206
|
+
issuer: tokenSet.claims.iss,
|
|
207
|
+
email: profile.email,
|
|
208
|
+
accessToken: encryptedAccessToken,
|
|
209
|
+
idToken: encryptedIdToken,
|
|
210
|
+
refreshToken: encryptedRefreshToken,
|
|
211
|
+
scope: tokenSet.scope || "",
|
|
212
|
+
expiresAt,
|
|
213
|
+
createdAt: new Date(),
|
|
214
|
+
updatedAt: new Date(),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Return JWT token in requested format
|
|
221
|
+
return formatTokenResponse(token, user, redirectUrl || session.redirectUri, response_type);
|
|
222
|
+
} catch (error: any) {
|
|
223
|
+
log.error("OIDC callback error:", error);
|
|
224
|
+
|
|
225
|
+
// Handle OIDC-specific errors
|
|
226
|
+
if (error.code && Object.values(OidcErrorCodes).includes(error.code)) {
|
|
227
|
+
// Call onAuthError callback if provided
|
|
228
|
+
const { options } = ctx.plugins.oidc;
|
|
229
|
+
if (options.onAuthError) {
|
|
230
|
+
try {
|
|
231
|
+
const errorResult = await options.onAuthError({
|
|
232
|
+
error,
|
|
233
|
+
provider,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (errorResult.redirectUrl) {
|
|
237
|
+
return {
|
|
238
|
+
status: 302,
|
|
239
|
+
headers: { Location: errorResult.redirectUrl },
|
|
240
|
+
data: {},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
} catch (callbackError) {
|
|
244
|
+
log.error("onAuthError callback failed:", callbackError);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return badRequest(error.message);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Handle provider errors
|
|
252
|
+
const mappedError = handleProviderError(error);
|
|
253
|
+
return internalServerError(mappedError.message);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export default CallbackOidc;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OIDC Initiate Handler
|
|
3
|
+
*
|
|
4
|
+
* Initiates the OIDC authorization code flow by:
|
|
5
|
+
* 1. Validating the provider is supported and configured
|
|
6
|
+
* 2. Generating cryptographically secure state, code_verifier, and nonce
|
|
7
|
+
* 3. Creating an OIDC session to track the flow
|
|
8
|
+
* 4. Building the provider's authorization URL with PKCE
|
|
9
|
+
* 5. Redirecting the user to the provider for authorization
|
|
10
|
+
*
|
|
11
|
+
* Route: GET /oidc/:provider/initiate?redirectUri={optional_redirect_url}
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { GetHandler, HttpMethod, RouteProps, badRequest, internalServerError } from "@flink-app/flink";
|
|
15
|
+
import InitiateRequest from "../schemas/InitiateRequest";
|
|
16
|
+
import { generateState, generateSessionId, generateNonce } from "../utils/state-utils";
|
|
17
|
+
import { validateProvider, createOidcError, OidcErrorCodes } from "../utils/error-utils";
|
|
18
|
+
import { generators } from "openid-client";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Path parameters for the handler
|
|
22
|
+
*/
|
|
23
|
+
interface PathParams {
|
|
24
|
+
provider: string;
|
|
25
|
+
[key: string]: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Route configuration
|
|
30
|
+
* This handler is registered programmatically by the plugin
|
|
31
|
+
*/
|
|
32
|
+
export const Route: RouteProps = {
|
|
33
|
+
path: "/oidc/:provider/initiate",
|
|
34
|
+
method: HttpMethod.get,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* OIDC Initiate Handler
|
|
39
|
+
*
|
|
40
|
+
* Starts the OIDC flow by generating security parameters, creating a session,
|
|
41
|
+
* and redirecting to the OIDC provider's authorization URL.
|
|
42
|
+
*/
|
|
43
|
+
const InitiateOidc: GetHandler<any, any, PathParams, InitiateRequest> = async ({ ctx, req }) => {
|
|
44
|
+
const { provider } = req.params;
|
|
45
|
+
const { redirectUri } = req.query;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Validate provider name format
|
|
49
|
+
validateProvider(provider);
|
|
50
|
+
|
|
51
|
+
// Get provider registry from context
|
|
52
|
+
const providerRegistry = (ctx as any).oidcProviderRegistry;
|
|
53
|
+
if (!providerRegistry) {
|
|
54
|
+
throw createOidcError(OidcErrorCodes.PROVIDER_NOT_CONFIGURED, "OIDC plugin not properly initialized");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Get OIDC provider instance (loads dynamically if needed)
|
|
58
|
+
const oidcProvider = await providerRegistry.getProvider(provider);
|
|
59
|
+
|
|
60
|
+
// Get plugin options
|
|
61
|
+
const { options } = ctx.plugins.oidc;
|
|
62
|
+
|
|
63
|
+
// Determine redirect URI (use provided or default to callback URL)
|
|
64
|
+
const staticProviderConfig = options.providers[provider];
|
|
65
|
+
const finalRedirectUri = redirectUri || staticProviderConfig?.callbackUrl || `${req.protocol}://${req.get("host")}/oidc/${provider}/callback`;
|
|
66
|
+
|
|
67
|
+
// Generate cryptographically secure parameters
|
|
68
|
+
const state = generateState();
|
|
69
|
+
const sessionId = generateSessionId();
|
|
70
|
+
const nonce = generateNonce();
|
|
71
|
+
const codeVerifier = generators.codeVerifier();
|
|
72
|
+
|
|
73
|
+
// Store session for state validation in callback
|
|
74
|
+
await ctx.repos.oidcSessionRepo.create({
|
|
75
|
+
sessionId,
|
|
76
|
+
state,
|
|
77
|
+
codeVerifier,
|
|
78
|
+
nonce,
|
|
79
|
+
provider,
|
|
80
|
+
redirectUri: finalRedirectUri,
|
|
81
|
+
createdAt: new Date(),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Build authorization URL with PKCE and nonce
|
|
85
|
+
const authorizationUrl = await oidcProvider.getAuthorizationUrl({
|
|
86
|
+
state,
|
|
87
|
+
codeVerifier,
|
|
88
|
+
nonce,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Redirect user to provider's authorization page
|
|
92
|
+
return {
|
|
93
|
+
status: 302,
|
|
94
|
+
headers: {
|
|
95
|
+
Location: authorizationUrl,
|
|
96
|
+
},
|
|
97
|
+
data: {},
|
|
98
|
+
};
|
|
99
|
+
} catch (error: any) {
|
|
100
|
+
// Handle validation errors
|
|
101
|
+
if (error.code && Object.values(OidcErrorCodes).includes(error.code)) {
|
|
102
|
+
return badRequest(error.message);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Handle unexpected errors
|
|
106
|
+
return internalServerError(error.message || "Failed to initiate OIDC flow");
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export default InitiateOidc;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @flink-app/oidc-plugin
|
|
3
|
+
*
|
|
4
|
+
* OIDC authentication plugin for Flink Framework
|
|
5
|
+
*
|
|
6
|
+
* Provides OpenID Connect authentication with generic IdP support,
|
|
7
|
+
* JWT integration via jwt-auth-plugin, JIT user provisioning,
|
|
8
|
+
* and optional token storage for API access.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Main plugin factory
|
|
12
|
+
export { oidcPlugin } from "./OidcPlugin";
|
|
13
|
+
|
|
14
|
+
// Configuration types
|
|
15
|
+
export { OidcPluginOptions, OidcError, AuthSuccessCallbackResponse, AuthErrorCallbackResponse } from "./OidcPluginOptions";
|
|
16
|
+
export { OidcProviderConfig } from "./OidcProviderConfig";
|
|
17
|
+
|
|
18
|
+
// Context types
|
|
19
|
+
export { OidcPluginContext } from "./OidcPluginContext";
|
|
20
|
+
|
|
21
|
+
// Schema types
|
|
22
|
+
export { default as OidcProfile } from "./schemas/OidcProfile";
|
|
23
|
+
export { default as OidcTokenSet } from "./schemas/OidcTokenSet";
|
|
24
|
+
export { default as OidcSession } from "./schemas/OidcSession";
|
|
25
|
+
export { default as OidcConnection } from "./schemas/OidcConnection";
|
|
26
|
+
export { default as InitiateRequest } from "./schemas/InitiateRequest";
|
|
27
|
+
export { default as CallbackRequest } from "./schemas/CallbackRequest";
|
|
28
|
+
|
|
29
|
+
// Provider classes (for advanced usage)
|
|
30
|
+
export { OidcProvider } from "./providers/OidcProvider";
|
|
31
|
+
export { ProviderRegistry } from "./providers/ProviderRegistry";
|
|
32
|
+
|
|
33
|
+
// Utility functions (for custom implementations)
|
|
34
|
+
export { generateState, generateSessionId, generateNonce, validateState } from "./utils/state-utils";
|
|
35
|
+
export { encryptToken, decryptToken, validateEncryptionSecret } from "./utils/encryption-utils";
|
|
36
|
+
export { mapClaimsToProfile, extractCustomClaims } from "./utils/claims-mapper";
|
|
37
|
+
export { formatTokenResponse } from "./utils/response-utils";
|
|
38
|
+
export { createOidcError, validateProvider, handleProviderError, OidcErrorCodes } from "./utils/error-utils";
|