@flink-app/oidc-plugin 0.14.3 → 2.0.0-alpha.100
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 +485 -0
- package/README.md +23 -2
- package/dist/OidcPlugin.d.ts +1 -1
- package/dist/OidcPlugin.d.ts.map +1 -1
- package/dist/OidcPlugin.js +32 -18
- package/dist/OidcPluginOptions.d.ts +75 -8
- package/dist/OidcPluginOptions.d.ts.map +1 -1
- package/dist/OidcProviderConfig.d.ts +21 -1
- package/dist/OidcProviderConfig.d.ts.map +1 -1
- package/dist/handlers/CallbackOidc.d.ts.map +1 -1
- package/dist/handlers/CallbackOidc.js +50 -5
- package/dist/handlers/InitiateOidc.d.ts.map +1 -1
- package/dist/handlers/InitiateOidc.js +14 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/log.d.ts +27 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +37 -0
- package/dist/providers/OidcProvider.d.ts +1 -1
- package/dist/providers/OidcProvider.d.ts.map +1 -1
- package/dist/providers/OidcProvider.js +46 -5
- package/dist/providers/ProviderRegistry.d.ts +2 -1
- package/dist/providers/ProviderRegistry.d.ts.map +1 -1
- package/dist/providers/ProviderRegistry.js +10 -2
- package/dist/schemas/CallbackRequest.d.ts +3 -6
- package/dist/schemas/CallbackRequest.d.ts.map +1 -1
- package/dist/schemas/InitiateRequest.d.ts +3 -6
- package/dist/schemas/InitiateRequest.d.ts.map +1 -1
- package/dist/schemas/OidcSession.d.ts +5 -0
- package/dist/schemas/OidcSession.d.ts.map +1 -1
- package/dist/utils/response-utils.d.ts +16 -5
- package/dist/utils/response-utils.d.ts.map +1 -1
- package/dist/utils/response-utils.js +31 -10
- package/examples/multi-provider.ts +1 -1
- package/package.json +8 -11
- package/spec/helpers/test-helpers.ts +14 -0
- package/spec/plugin/OidcPlugin.spec.ts +104 -3
- package/spec/providers/ProviderRegistry.spec.ts +8 -7
- package/spec/utils/response-utils.spec.ts +85 -0
- package/src/OidcPlugin.ts +54 -21
- package/src/OidcPluginOptions.ts +84 -9
- package/src/OidcProviderConfig.ts +23 -1
- package/src/handlers/CallbackOidc.ts +66 -5
- package/src/handlers/InitiateOidc.ts +19 -1
- package/src/index.ts +1 -1
- package/src/log.ts +33 -0
- package/src/providers/OidcProvider.ts +60 -7
- package/src/providers/ProviderRegistry.ts +22 -3
- package/src/schemas/CallbackRequest.ts +4 -6
- package/src/schemas/InitiateRequest.ts +3 -6
- package/src/schemas/OidcSession.ts +6 -0
- package/src/utils/response-utils.ts +53 -10
|
@@ -16,6 +16,7 @@ import InitiateRequest from "../schemas/InitiateRequest";
|
|
|
16
16
|
import { generateState, generateSessionId, generateNonce } from "../utils/state-utils";
|
|
17
17
|
import { validateProvider, createOidcError, OidcErrorCodes } from "../utils/error-utils";
|
|
18
18
|
import { generators } from "openid-client";
|
|
19
|
+
import { oidcLog } from "../log";
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Path parameters for the handler
|
|
@@ -48,6 +49,8 @@ const InitiateOidc: GetHandler<any, any, PathParams, InitiateRequest> = async ({
|
|
|
48
49
|
// Validate provider name format
|
|
49
50
|
validateProvider(provider);
|
|
50
51
|
|
|
52
|
+
oidcLog.debug(`Initiate: provider="${provider}" redirectUri="${redirectUri || "(not set)"}"`);
|
|
53
|
+
|
|
51
54
|
// Get provider registry from context
|
|
52
55
|
const providerRegistry = (ctx as any).oidcProviderRegistry;
|
|
53
56
|
if (!providerRegistry) {
|
|
@@ -61,9 +64,19 @@ const InitiateOidc: GetHandler<any, any, PathParams, InitiateRequest> = async ({
|
|
|
61
64
|
const { options } = ctx.plugins.oidc;
|
|
62
65
|
|
|
63
66
|
// Determine redirect URI (use provided or default to callback URL)
|
|
64
|
-
const staticProviderConfig = options.providers[provider];
|
|
67
|
+
const staticProviderConfig = options.providers?.[provider];
|
|
65
68
|
const finalRedirectUri = redirectUri || staticProviderConfig?.callbackUrl || `${req.protocol}://${req.get("host")}/oidc/${provider}/callback`;
|
|
66
69
|
|
|
70
|
+
// Collect meta.* query params as opaque metadata (prefix stripped)
|
|
71
|
+
const metadata: Record<string, string> = {};
|
|
72
|
+
for (const [key, value] of Object.entries(req.query)) {
|
|
73
|
+
if (key.startsWith("meta.") && typeof value === "string") {
|
|
74
|
+
metadata[key.slice(5)] = value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
oidcLog.debug(`Initiate: resolved redirectUri="${finalRedirectUri}" (source: ${redirectUri ? "query param" : staticProviderConfig?.callbackUrl ? "provider config" : "auto-detected"})`);
|
|
79
|
+
|
|
67
80
|
// Generate cryptographically secure parameters
|
|
68
81
|
const state = generateState();
|
|
69
82
|
const sessionId = generateSessionId();
|
|
@@ -78,9 +91,12 @@ const InitiateOidc: GetHandler<any, any, PathParams, InitiateRequest> = async ({
|
|
|
78
91
|
nonce,
|
|
79
92
|
provider,
|
|
80
93
|
redirectUri: finalRedirectUri,
|
|
94
|
+
metadata,
|
|
81
95
|
createdAt: new Date(),
|
|
82
96
|
});
|
|
83
97
|
|
|
98
|
+
oidcLog.debug(`Initiate: session created sessionId=${sessionId}`);
|
|
99
|
+
|
|
84
100
|
// Build authorization URL with PKCE and nonce
|
|
85
101
|
const authorizationUrl = await oidcProvider.getAuthorizationUrl({
|
|
86
102
|
state,
|
|
@@ -88,6 +104,8 @@ const InitiateOidc: GetHandler<any, any, PathParams, InitiateRequest> = async ({
|
|
|
88
104
|
nonce,
|
|
89
105
|
});
|
|
90
106
|
|
|
107
|
+
oidcLog.debug(`Initiate: redirecting to IdP authorization URL: ${authorizationUrl}`);
|
|
108
|
+
|
|
91
109
|
// Redirect user to provider's authorization page
|
|
92
110
|
return {
|
|
93
111
|
status: 302,
|
package/src/index.ts
CHANGED
|
@@ -34,5 +34,5 @@ export { ProviderRegistry } from "./providers/ProviderRegistry";
|
|
|
34
34
|
export { generateState, generateSessionId, generateNonce, validateState } from "./utils/state-utils";
|
|
35
35
|
export { encryptToken, decryptToken, validateEncryptionSecret } from "./utils/encryption-utils";
|
|
36
36
|
export { mapClaimsToProfile, extractCustomClaims } from "./utils/claims-mapper";
|
|
37
|
-
export { formatTokenResponse } from "./utils/response-utils";
|
|
37
|
+
export { formatTokenResponse, TokenDeliveryMode } from "./utils/response-utils";
|
|
38
38
|
export { createOidcError, validateProvider, handleProviderError, OidcErrorCodes } from "./utils/error-utils";
|
package/src/log.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { FlinkLogFactory } from "@flink-app/flink";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Named logger for the OIDC plugin.
|
|
5
|
+
*
|
|
6
|
+
* Enables independent log level control via flink.config.js:
|
|
7
|
+
* ```js
|
|
8
|
+
* module.exports = {
|
|
9
|
+
* logging: {
|
|
10
|
+
* components: {
|
|
11
|
+
* "flink.oidc": "debug"
|
|
12
|
+
* }
|
|
13
|
+
* }
|
|
14
|
+
* };
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Or at runtime:
|
|
18
|
+
* ```ts
|
|
19
|
+
* FlinkLogFactory.setComponentLevel("flink.oidc", "debug");
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export const oidcLog = FlinkLogFactory.createLogger("flink.oidc");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Mask a secret value for safe logging.
|
|
26
|
+
* Shows the first 4 characters followed by **** to help identify which secret
|
|
27
|
+
* is in use without exposing the full value.
|
|
28
|
+
*/
|
|
29
|
+
export function maskSecret(value: string | undefined): string {
|
|
30
|
+
if (!value) return "(not set)";
|
|
31
|
+
if (value.length <= 4) return "****";
|
|
32
|
+
return `${value.slice(0, 4)}****`;
|
|
33
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { Issuer, Client, generators, TokenSet, UserinfoResponse } from "openid-client";
|
|
1
|
+
import { Issuer, Client, ClientMetadata, generators, TokenSet, UserinfoResponse } from "openid-client";
|
|
2
2
|
import { OidcProviderConfig } from "../OidcProviderConfig";
|
|
3
3
|
import OidcProfile from "../schemas/OidcProfile";
|
|
4
4
|
import OidcTokenSet from "../schemas/OidcTokenSet";
|
|
5
5
|
import { mapClaimsToProfile, extractCustomClaims } from "../utils/claims-mapper";
|
|
6
6
|
import { createOidcError, OidcErrorCodes } from "../utils/error-utils";
|
|
7
|
+
import { oidcLog } from "../log";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Generic OIDC Provider implementation using openid-client
|
|
@@ -17,7 +18,7 @@ import { createOidcError, OidcErrorCodes } from "../utils/error-utils";
|
|
|
17
18
|
* - Claims mapping to profile
|
|
18
19
|
*/
|
|
19
20
|
export class OidcProvider {
|
|
20
|
-
|
|
21
|
+
public readonly config: OidcProviderConfig;
|
|
21
22
|
private issuer: Issuer<Client> | null = null;
|
|
22
23
|
private client: Client | null = null;
|
|
23
24
|
private initialized: boolean = false;
|
|
@@ -42,7 +43,19 @@ export class OidcProvider {
|
|
|
42
43
|
try {
|
|
43
44
|
// Option 1: OIDC Discovery
|
|
44
45
|
if (this.config.discoveryUrl) {
|
|
46
|
+
oidcLog.debug(`Provider "${this.config.issuer}": discovering from ${this.config.discoveryUrl}`);
|
|
45
47
|
this.issuer = await Issuer.discover(this.config.discoveryUrl);
|
|
48
|
+
oidcLog.debug(
|
|
49
|
+
`Provider "${this.config.issuer}": discovery complete`,
|
|
50
|
+
`authorization_endpoint=${this.issuer.metadata.authorization_endpoint}`,
|
|
51
|
+
`token_endpoint=${this.issuer.metadata.token_endpoint}`,
|
|
52
|
+
`userinfo_endpoint=${this.issuer.metadata.userinfo_endpoint ?? "(none)"}`,
|
|
53
|
+
`jwks_uri=${this.issuer.metadata.jwks_uri}`
|
|
54
|
+
);
|
|
55
|
+
oidcLog.trace(
|
|
56
|
+
`Provider "${this.config.issuer}": raw discovery metadata from ${this.config.discoveryUrl}`,
|
|
57
|
+
this.issuer.metadata
|
|
58
|
+
);
|
|
46
59
|
}
|
|
47
60
|
// Option 2: Manual configuration
|
|
48
61
|
else {
|
|
@@ -65,12 +78,23 @@ export class OidcProvider {
|
|
|
65
78
|
}
|
|
66
79
|
|
|
67
80
|
// Create OIDC client
|
|
68
|
-
|
|
81
|
+
const clientMetadata: ClientMetadata = {
|
|
69
82
|
client_id: this.config.clientId,
|
|
70
|
-
client_secret: this.config.clientSecret,
|
|
71
83
|
redirect_uris: [this.config.callbackUrl],
|
|
72
84
|
response_types: ["code"],
|
|
73
|
-
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (this.config.clientSecret) {
|
|
88
|
+
clientMetadata.client_secret = this.config.clientSecret;
|
|
89
|
+
clientMetadata.token_endpoint_auth_method =
|
|
90
|
+
this.config.tokenEndpointAuthMethod || "client_secret_basic";
|
|
91
|
+
} else {
|
|
92
|
+
// Public client (PKCE-only) — openid-client requires this to skip client authentication
|
|
93
|
+
clientMetadata.token_endpoint_auth_method =
|
|
94
|
+
this.config.tokenEndpointAuthMethod || "none";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.client = new this.issuer.Client(clientMetadata);
|
|
74
98
|
|
|
75
99
|
this.initialized = true;
|
|
76
100
|
} catch (error: any) {
|
|
@@ -129,8 +153,14 @@ export class OidcProvider {
|
|
|
129
153
|
}
|
|
130
154
|
);
|
|
131
155
|
|
|
156
|
+
// Raw response from the token endpoint (includes access_token, id_token,
|
|
157
|
+
// refresh_token, token_type, expires_at, scope, and any non-standard fields
|
|
158
|
+
// returned by the IdP). Trace-level because it contains secrets.
|
|
159
|
+
oidcLog.trace(`Provider "${this.config.issuer}": raw token endpoint response`, { ...tokenSet });
|
|
160
|
+
|
|
132
161
|
// Extract claims from ID token (already validated by openid-client)
|
|
133
162
|
const claims = tokenSet.claims();
|
|
163
|
+
oidcLog.trace(`Provider "${this.config.issuer}": extracted claims from ID token`, claims);
|
|
134
164
|
|
|
135
165
|
return {
|
|
136
166
|
accessToken: tokenSet.access_token!,
|
|
@@ -142,6 +172,15 @@ export class OidcProvider {
|
|
|
142
172
|
claims,
|
|
143
173
|
};
|
|
144
174
|
} catch (error: any) {
|
|
175
|
+
// openid-client attaches the raw IdP error response on OPError.
|
|
176
|
+
oidcLog.trace(`Provider "${this.config.issuer}": raw token exchange error`, {
|
|
177
|
+
name: error.name,
|
|
178
|
+
message: error.message,
|
|
179
|
+
error: error.error,
|
|
180
|
+
error_description: error.error_description,
|
|
181
|
+
error_uri: error.error_uri,
|
|
182
|
+
response: error.response?.body ?? error.response,
|
|
183
|
+
});
|
|
145
184
|
throw createOidcError(OidcErrorCodes.TOKEN_EXCHANGE_FAILED, `Token exchange failed: ${error.message}`, {
|
|
146
185
|
originalError: error.message,
|
|
147
186
|
errorCode: error.error,
|
|
@@ -163,8 +202,17 @@ export class OidcProvider {
|
|
|
163
202
|
|
|
164
203
|
try {
|
|
165
204
|
const userinfo = await this.client!.userinfo(accessToken);
|
|
205
|
+
// Raw response from the UserInfo endpoint, before any mapping/merging.
|
|
206
|
+
oidcLog.trace(`Provider "${this.config.issuer}": raw UserInfo endpoint response`, userinfo);
|
|
166
207
|
return userinfo;
|
|
167
208
|
} catch (error: any) {
|
|
209
|
+
oidcLog.trace(`Provider "${this.config.issuer}": raw UserInfo error`, {
|
|
210
|
+
name: error.name,
|
|
211
|
+
message: error.message,
|
|
212
|
+
error: error.error,
|
|
213
|
+
error_description: error.error_description,
|
|
214
|
+
response: error.response?.body ?? error.response,
|
|
215
|
+
});
|
|
168
216
|
throw createOidcError(OidcErrorCodes.USERINFO_FAILED, `UserInfo request failed: ${error.message}`, {
|
|
169
217
|
originalError: error.message,
|
|
170
218
|
});
|
|
@@ -185,14 +233,17 @@ export class OidcProvider {
|
|
|
185
233
|
let claims = { ...tokenSet.claims };
|
|
186
234
|
|
|
187
235
|
// Optionally fetch additional claims from UserInfo endpoint
|
|
188
|
-
|
|
236
|
+
// Check both manual config and discovered endpoint (via OIDC discovery)
|
|
237
|
+
const userinfoUrl = this.config.userinfoEndpoint || this.issuer?.metadata?.userinfo_endpoint;
|
|
238
|
+
|
|
239
|
+
if (includeUserInfo && userinfoUrl) {
|
|
189
240
|
try {
|
|
190
241
|
const userinfo = await this.getUserInfo(tokenSet.accessToken);
|
|
191
242
|
// Merge UserInfo claims with ID token claims
|
|
192
243
|
claims = { ...claims, ...userinfo };
|
|
193
244
|
} catch (error) {
|
|
194
245
|
// UserInfo is optional - continue with ID token claims only
|
|
195
|
-
|
|
246
|
+
oidcLog.warn(`Failed to fetch UserInfo from ${userinfoUrl}, using ID token claims only:`, error);
|
|
196
247
|
}
|
|
197
248
|
}
|
|
198
249
|
|
|
@@ -200,10 +251,12 @@ export class OidcProvider {
|
|
|
200
251
|
if (this.config.claimMapping) {
|
|
201
252
|
const customClaims = extractCustomClaims(claims, this.config.claimMapping);
|
|
202
253
|
claims = { ...claims, ...customClaims };
|
|
254
|
+
oidcLog.trace(`Provider "${this.config.issuer}": applied custom claim mapping`, claims);
|
|
203
255
|
}
|
|
204
256
|
|
|
205
257
|
// Map to normalized profile
|
|
206
258
|
const profile = mapClaimsToProfile(claims);
|
|
259
|
+
oidcLog.trace(`Provider "${this.config.issuer}": built final profile`, profile);
|
|
207
260
|
|
|
208
261
|
return profile;
|
|
209
262
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { OidcProvider } from "./OidcProvider";
|
|
2
2
|
import { OidcProviderConfig } from "../OidcProviderConfig";
|
|
3
3
|
import { createOidcError, OidcErrorCodes } from "../utils/error-utils";
|
|
4
|
+
import { oidcLog, maskSecret } from "../log";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Provider registry for managing OIDC provider instances
|
|
@@ -14,14 +15,17 @@ import { createOidcError, OidcErrorCodes } from "../utils/error-utils";
|
|
|
14
15
|
export class ProviderRegistry {
|
|
15
16
|
private staticProviders: Record<string, OidcProviderConfig>;
|
|
16
17
|
private providerInstances: Map<string, OidcProvider> = new Map();
|
|
17
|
-
private providerLoader?: (providerName: string) => Promise<OidcProviderConfig | null>;
|
|
18
|
+
private providerLoader?: (providerName: string, ctx: any) => Promise<OidcProviderConfig | null>;
|
|
19
|
+
private ctx: any;
|
|
18
20
|
|
|
19
21
|
constructor(
|
|
20
22
|
staticProviders: Record<string, OidcProviderConfig>,
|
|
21
|
-
providerLoader?: (providerName: string) => Promise<OidcProviderConfig | null
|
|
23
|
+
providerLoader?: (providerName: string, ctx: any) => Promise<OidcProviderConfig | null>,
|
|
24
|
+
ctx?: any
|
|
22
25
|
) {
|
|
23
26
|
this.staticProviders = staticProviders;
|
|
24
27
|
this.providerLoader = providerLoader;
|
|
28
|
+
this.ctx = ctx;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
/**
|
|
@@ -42,24 +46,39 @@ export class ProviderRegistry {
|
|
|
42
46
|
// Check cache first
|
|
43
47
|
const cachedProvider = this.providerInstances.get(providerName);
|
|
44
48
|
if (cachedProvider) {
|
|
49
|
+
oidcLog.debug(`Provider "${providerName}": using cached instance`);
|
|
45
50
|
return cachedProvider;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
// Try static configuration
|
|
49
54
|
let config: OidcProviderConfig | null = this.staticProviders[providerName] || null;
|
|
55
|
+
let configSource = "static";
|
|
50
56
|
|
|
51
57
|
// Try dynamic loader if not in static config
|
|
52
58
|
if (!config && this.providerLoader) {
|
|
53
|
-
config
|
|
59
|
+
oidcLog.debug(`Provider "${providerName}": not in static config, invoking providerLoader`);
|
|
60
|
+
config = await this.providerLoader(providerName, this.ctx);
|
|
61
|
+
configSource = "dynamic";
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
if (!config) {
|
|
65
|
+
oidcLog.debug(`Provider "${providerName}": not found (static=[${Object.keys(this.staticProviders).join(", ")}], loader=${!!this.providerLoader})`);
|
|
57
66
|
throw createOidcError(OidcErrorCodes.PROVIDER_NOT_CONFIGURED, `OIDC provider '${providerName}' is not configured`, {
|
|
58
67
|
providerName,
|
|
59
68
|
availableProviders: Object.keys(this.staticProviders),
|
|
60
69
|
});
|
|
61
70
|
}
|
|
62
71
|
|
|
72
|
+
oidcLog.debug(
|
|
73
|
+
`Provider "${providerName}" resolved via ${configSource}:`,
|
|
74
|
+
`issuer=${config.issuer}`,
|
|
75
|
+
`clientId=${config.clientId}`,
|
|
76
|
+
`clientSecret=${maskSecret(config.clientSecret)}`,
|
|
77
|
+
`callbackUrl=${config.callbackUrl}`,
|
|
78
|
+
config.discoveryUrl ? `discoveryUrl=${config.discoveryUrl}` : `authorizationEndpoint=${config.authorizationEndpoint}`,
|
|
79
|
+
`scope=${(config.scope || ["openid", "email", "profile"]).join(" ")}`
|
|
80
|
+
);
|
|
81
|
+
|
|
63
82
|
// Create and initialize provider
|
|
64
83
|
const provider = new OidcProvider(config);
|
|
65
84
|
await provider.initialize();
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* GET /oidc/:provider/callback?code=...&state=...&response_type=json
|
|
5
5
|
*/
|
|
6
|
-
|
|
6
|
+
type CallbackRequest = Record<string, string | string[]> & {
|
|
7
7
|
/**
|
|
8
8
|
* Authorization code from the IdP
|
|
9
9
|
* Required for successful authentication
|
|
@@ -34,8 +34,6 @@ export default interface CallbackRequest {
|
|
|
34
34
|
*/
|
|
35
35
|
response_type?: "json";
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
[key: string]: string | string[] | undefined;
|
|
41
|
-
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default CallbackRequest;
|
|
@@ -3,15 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* GET /oidc/:provider/initiate?redirectUri=...
|
|
5
5
|
*/
|
|
6
|
-
|
|
6
|
+
type InitiateRequest = Record<string, string | string[]> & {
|
|
7
7
|
/**
|
|
8
8
|
* Optional redirect URI after successful authentication
|
|
9
9
|
* If not provided, uses the default callbackUrl from provider config
|
|
10
10
|
*/
|
|
11
11
|
redirectUri?: string;
|
|
12
|
+
};
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
* Index signature for Flink Query type compatibility
|
|
15
|
-
*/
|
|
16
|
-
[key: string]: string | string[] | undefined;
|
|
17
|
-
}
|
|
14
|
+
export default InitiateRequest;
|
|
@@ -44,6 +44,12 @@ export default interface OidcSession {
|
|
|
44
44
|
*/
|
|
45
45
|
redirectUri: string;
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Arbitrary metadata collected from meta.* query parameters on initiate
|
|
49
|
+
* Passed through to onAuthSuccess and onAuthError callbacks unchanged
|
|
50
|
+
*/
|
|
51
|
+
metadata?: Record<string, string>;
|
|
52
|
+
|
|
47
53
|
/**
|
|
48
54
|
* Session creation timestamp
|
|
49
55
|
* MongoDB TTL index will automatically delete expired sessions
|
|
@@ -1,20 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Controls how the JWT token is delivered back to the client on the
|
|
3
|
+
* post-callback redirect.
|
|
4
|
+
*
|
|
5
|
+
* - "fragment": appends `#token=...` to the redirectUrl. Best for web
|
|
6
|
+
* browsers — URL fragments are not sent to the server in HTTP requests
|
|
7
|
+
* and are only accessible to client-side JavaScript.
|
|
8
|
+
* - "query": appends `?token=...` (or `&token=...`). Required for native
|
|
9
|
+
* mobile flows where the callback comes back via a custom URL scheme,
|
|
10
|
+
* because iOS `ASWebAuthenticationSession` strips URL fragments before
|
|
11
|
+
* delivering the URL to the app.
|
|
12
|
+
*/
|
|
13
|
+
export type TokenDeliveryMode = "fragment" | "query";
|
|
14
|
+
|
|
1
15
|
/**
|
|
2
16
|
* Format token response for the client
|
|
3
17
|
*
|
|
4
18
|
* Supports two response formats:
|
|
5
19
|
* 1. JSON response (for API clients)
|
|
6
|
-
* 2. Redirect with token in URL fragment (for web
|
|
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.
|
|
20
|
+
* 2. Redirect with token in URL fragment or query (for web/native clients)
|
|
10
21
|
*
|
|
11
22
|
* @param token - JWT token for the application
|
|
12
23
|
* @param user - User object
|
|
13
24
|
* @param redirectUrl - URL to redirect to
|
|
14
25
|
* @param responseType - "json" or undefined (redirect)
|
|
26
|
+
* @param tokenDelivery - How to attach the token on redirect. Defaults to "fragment".
|
|
15
27
|
* @returns Flink response object
|
|
16
28
|
*/
|
|
17
|
-
export function formatTokenResponse(
|
|
29
|
+
export function formatTokenResponse(
|
|
30
|
+
token: string,
|
|
31
|
+
user: any,
|
|
32
|
+
redirectUrl: string,
|
|
33
|
+
responseType?: "json",
|
|
34
|
+
tokenDelivery: TokenDeliveryMode = "fragment"
|
|
35
|
+
): any {
|
|
18
36
|
// JSON response for API clients
|
|
19
37
|
if (responseType === "json") {
|
|
20
38
|
return {
|
|
@@ -25,11 +43,8 @@ export function formatTokenResponse(token: string, user: any, redirectUrl: strin
|
|
|
25
43
|
};
|
|
26
44
|
}
|
|
27
45
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const separator = redirectUrl.includes("#") ? "&" : "#";
|
|
31
|
-
const tokenFragment = `token=${encodeURIComponent(token)}`;
|
|
32
|
-
const finalUrl = `${redirectUrl}${separator}${tokenFragment}`;
|
|
46
|
+
const tokenParam = `token=${encodeURIComponent(token)}`;
|
|
47
|
+
const finalUrl = appendToken(redirectUrl, tokenParam, tokenDelivery);
|
|
33
48
|
|
|
34
49
|
return {
|
|
35
50
|
status: 302,
|
|
@@ -39,3 +54,31 @@ export function formatTokenResponse(token: string, user: any, redirectUrl: strin
|
|
|
39
54
|
data: {},
|
|
40
55
|
};
|
|
41
56
|
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Append a token parameter to a URL using the specified delivery mode.
|
|
60
|
+
*
|
|
61
|
+
* For "query" mode, any existing `#fragment` on the URL is preserved: the
|
|
62
|
+
* query parameter is spliced in *before* the fragment so the final URL is
|
|
63
|
+
* still well-formed.
|
|
64
|
+
*
|
|
65
|
+
* For "fragment" mode, a second token on an existing fragment is joined
|
|
66
|
+
* with `&` (same as the old behavior).
|
|
67
|
+
*/
|
|
68
|
+
function appendToken(redirectUrl: string, tokenParam: string, mode: TokenDeliveryMode): string {
|
|
69
|
+
if (mode === "query") {
|
|
70
|
+
const hashIdx = redirectUrl.indexOf("#");
|
|
71
|
+
if (hashIdx >= 0) {
|
|
72
|
+
const base = redirectUrl.slice(0, hashIdx);
|
|
73
|
+
const frag = redirectUrl.slice(hashIdx);
|
|
74
|
+
const sep = base.includes("?") ? "&" : "?";
|
|
75
|
+
return `${base}${sep}${tokenParam}${frag}`;
|
|
76
|
+
}
|
|
77
|
+
const sep = redirectUrl.includes("?") ? "&" : "?";
|
|
78
|
+
return `${redirectUrl}${sep}${tokenParam}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// fragment (default)
|
|
82
|
+
const sep = redirectUrl.includes("#") ? "&" : "#";
|
|
83
|
+
return `${redirectUrl}${sep}${tokenParam}`;
|
|
84
|
+
}
|