@flink-app/oidc-plugin 1.0.0 → 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.
Files changed (52) hide show
  1. package/CHANGELOG.md +558 -0
  2. package/README.md +23 -2
  3. package/dist/OidcPlugin.d.ts +1 -1
  4. package/dist/OidcPlugin.d.ts.map +1 -1
  5. package/dist/OidcPlugin.js +32 -18
  6. package/dist/OidcPluginOptions.d.ts +75 -8
  7. package/dist/OidcPluginOptions.d.ts.map +1 -1
  8. package/dist/OidcProviderConfig.d.ts +21 -1
  9. package/dist/OidcProviderConfig.d.ts.map +1 -1
  10. package/dist/handlers/CallbackOidc.d.ts.map +1 -1
  11. package/dist/handlers/CallbackOidc.js +50 -5
  12. package/dist/handlers/InitiateOidc.d.ts.map +1 -1
  13. package/dist/handlers/InitiateOidc.js +14 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/log.d.ts +27 -0
  17. package/dist/log.d.ts.map +1 -0
  18. package/dist/log.js +37 -0
  19. package/dist/providers/OidcProvider.d.ts +1 -1
  20. package/dist/providers/OidcProvider.d.ts.map +1 -1
  21. package/dist/providers/OidcProvider.js +46 -5
  22. package/dist/providers/ProviderRegistry.d.ts +2 -1
  23. package/dist/providers/ProviderRegistry.d.ts.map +1 -1
  24. package/dist/providers/ProviderRegistry.js +10 -2
  25. package/dist/schemas/CallbackRequest.d.ts +3 -6
  26. package/dist/schemas/CallbackRequest.d.ts.map +1 -1
  27. package/dist/schemas/InitiateRequest.d.ts +3 -6
  28. package/dist/schemas/InitiateRequest.d.ts.map +1 -1
  29. package/dist/schemas/OidcSession.d.ts +5 -0
  30. package/dist/schemas/OidcSession.d.ts.map +1 -1
  31. package/dist/utils/response-utils.d.ts +16 -5
  32. package/dist/utils/response-utils.d.ts.map +1 -1
  33. package/dist/utils/response-utils.js +31 -10
  34. package/examples/multi-provider.ts +1 -1
  35. package/package.json +8 -11
  36. package/spec/helpers/test-helpers.ts +14 -0
  37. package/spec/plugin/OidcPlugin.spec.ts +104 -3
  38. package/spec/providers/ProviderRegistry.spec.ts +8 -7
  39. package/spec/utils/response-utils.spec.ts +85 -0
  40. package/src/OidcPlugin.ts +54 -21
  41. package/src/OidcPluginOptions.ts +84 -9
  42. package/src/OidcProviderConfig.ts +23 -1
  43. package/src/handlers/CallbackOidc.ts +66 -5
  44. package/src/handlers/InitiateOidc.ts +19 -1
  45. package/src/index.ts +1 -1
  46. package/src/log.ts +33 -0
  47. package/src/providers/OidcProvider.ts +60 -7
  48. package/src/providers/ProviderRegistry.ts +22 -3
  49. package/src/schemas/CallbackRequest.ts +4 -6
  50. package/src/schemas/InitiateRequest.ts +3 -6
  51. package/src/schemas/OidcSession.ts +6 -0
  52. package/src/utils/response-utils.ts +53 -10
@@ -1 +1 @@
1
- {"version":3,"file":"OidcProvider.d.ts","sourceRoot":"","sources":["../../src/providers/OidcProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwC,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACvF,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,WAAW,MAAM,wBAAwB,CAAC;AACjD,OAAO,YAAY,MAAM,yBAAyB,CAAC;AAInD;;;;;;;;;;GAUG;AACH,qBAAa,YAAY;IACrB,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,WAAW,CAAkB;gBAEzB,MAAM,EAAE,kBAAkB;IAItC;;;;;;;OAOG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IA+CjC;;;;;OAKG;IACG,mBAAmB,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAiB1G;;;;;;;OAOG;IACG,oBAAoB,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,YAAY,CAAC;IAqC/H;;;;;;;;OAQG;IACG,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAajE;;;;;;;;;OASG;IACG,YAAY,CAAC,QAAQ,EAAE,YAAY,EAAE,eAAe,GAAE,OAAc,GAAG,OAAO,CAAC,WAAW,CAAC;IA2BjG;;;;OAIG;YACW,iBAAiB;IAU/B;;;;OAIG;IACH,iBAAiB,IAAI,GAAG;CAM3B"}
1
+ {"version":3,"file":"OidcProvider.d.ts","sourceRoot":"","sources":["../../src/providers/OidcProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwD,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACvG,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,WAAW,MAAM,wBAAwB,CAAC;AACjD,OAAO,YAAY,MAAM,yBAAyB,CAAC;AAKnD;;;;;;;;;;GAUG;AACH,qBAAa,YAAY;IACrB,SAAgB,MAAM,EAAE,kBAAkB,CAAC;IAC3C,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,WAAW,CAAkB;gBAEzB,MAAM,EAAE,kBAAkB;IAItC;;;;;;;OAOG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAsEjC;;;;;OAKG;IACG,mBAAmB,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAiB1G;;;;;;;OAOG;IACG,oBAAoB,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,YAAY,CAAC;IAoD/H;;;;;;;;OAQG;IACG,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAsBjE;;;;;;;;;OASG;IACG,YAAY,CAAC,QAAQ,EAAE,YAAY,EAAE,eAAe,GAAE,OAAc,GAAG,OAAO,CAAC,WAAW,CAAC;IAgCjG;;;;OAIG;YACW,iBAAiB;IAU/B;;;;OAIG;IACH,iBAAiB,IAAI,GAAG;CAM3B"}
@@ -4,6 +4,7 @@ exports.OidcProvider = void 0;
4
4
  const openid_client_1 = require("openid-client");
5
5
  const claims_mapper_1 = require("../utils/claims-mapper");
6
6
  const error_utils_1 = require("../utils/error-utils");
7
+ const log_1 = require("../log");
7
8
  /**
8
9
  * Generic OIDC Provider implementation using openid-client
9
10
  *
@@ -37,7 +38,10 @@ class OidcProvider {
37
38
  try {
38
39
  // Option 1: OIDC Discovery
39
40
  if (this.config.discoveryUrl) {
41
+ log_1.oidcLog.debug(`Provider "${this.config.issuer}": discovering from ${this.config.discoveryUrl}`);
40
42
  this.issuer = await openid_client_1.Issuer.discover(this.config.discoveryUrl);
43
+ log_1.oidcLog.debug(`Provider "${this.config.issuer}": discovery complete`, `authorization_endpoint=${this.issuer.metadata.authorization_endpoint}`, `token_endpoint=${this.issuer.metadata.token_endpoint}`, `userinfo_endpoint=${this.issuer.metadata.userinfo_endpoint ?? "(none)"}`, `jwks_uri=${this.issuer.metadata.jwks_uri}`);
44
+ log_1.oidcLog.trace(`Provider "${this.config.issuer}": raw discovery metadata from ${this.config.discoveryUrl}`, this.issuer.metadata);
41
45
  }
42
46
  // Option 2: Manual configuration
43
47
  else {
@@ -54,12 +58,22 @@ class OidcProvider {
54
58
  });
55
59
  }
56
60
  // Create OIDC client
57
- this.client = new this.issuer.Client({
61
+ const clientMetadata = {
58
62
  client_id: this.config.clientId,
59
- client_secret: this.config.clientSecret,
60
63
  redirect_uris: [this.config.callbackUrl],
61
64
  response_types: ["code"],
62
- });
65
+ };
66
+ if (this.config.clientSecret) {
67
+ clientMetadata.client_secret = this.config.clientSecret;
68
+ clientMetadata.token_endpoint_auth_method =
69
+ this.config.tokenEndpointAuthMethod || "client_secret_basic";
70
+ }
71
+ else {
72
+ // Public client (PKCE-only) — openid-client requires this to skip client authentication
73
+ clientMetadata.token_endpoint_auth_method =
74
+ this.config.tokenEndpointAuthMethod || "none";
75
+ }
76
+ this.client = new this.issuer.Client(clientMetadata);
63
77
  this.initialized = true;
64
78
  }
65
79
  catch (error) {
@@ -107,8 +121,13 @@ class OidcProvider {
107
121
  state: params.state,
108
122
  nonce: params.nonce,
109
123
  });
124
+ // Raw response from the token endpoint (includes access_token, id_token,
125
+ // refresh_token, token_type, expires_at, scope, and any non-standard fields
126
+ // returned by the IdP). Trace-level because it contains secrets.
127
+ log_1.oidcLog.trace(`Provider "${this.config.issuer}": raw token endpoint response`, { ...tokenSet });
110
128
  // Extract claims from ID token (already validated by openid-client)
111
129
  const claims = tokenSet.claims();
130
+ log_1.oidcLog.trace(`Provider "${this.config.issuer}": extracted claims from ID token`, claims);
112
131
  return {
113
132
  accessToken: tokenSet.access_token,
114
133
  idToken: tokenSet.id_token,
@@ -120,6 +139,15 @@ class OidcProvider {
120
139
  };
121
140
  }
122
141
  catch (error) {
142
+ // openid-client attaches the raw IdP error response on OPError.
143
+ log_1.oidcLog.trace(`Provider "${this.config.issuer}": raw token exchange error`, {
144
+ name: error.name,
145
+ message: error.message,
146
+ error: error.error,
147
+ error_description: error.error_description,
148
+ error_uri: error.error_uri,
149
+ response: error.response?.body ?? error.response,
150
+ });
123
151
  throw (0, error_utils_1.createOidcError)(error_utils_1.OidcErrorCodes.TOKEN_EXCHANGE_FAILED, `Token exchange failed: ${error.message}`, {
124
152
  originalError: error.message,
125
153
  errorCode: error.error,
@@ -139,9 +167,18 @@ class OidcProvider {
139
167
  await this.ensureInitialized();
140
168
  try {
141
169
  const userinfo = await this.client.userinfo(accessToken);
170
+ // Raw response from the UserInfo endpoint, before any mapping/merging.
171
+ log_1.oidcLog.trace(`Provider "${this.config.issuer}": raw UserInfo endpoint response`, userinfo);
142
172
  return userinfo;
143
173
  }
144
174
  catch (error) {
175
+ log_1.oidcLog.trace(`Provider "${this.config.issuer}": raw UserInfo error`, {
176
+ name: error.name,
177
+ message: error.message,
178
+ error: error.error,
179
+ error_description: error.error_description,
180
+ response: error.response?.body ?? error.response,
181
+ });
145
182
  throw (0, error_utils_1.createOidcError)(error_utils_1.OidcErrorCodes.USERINFO_FAILED, `UserInfo request failed: ${error.message}`, {
146
183
  originalError: error.message,
147
184
  });
@@ -160,7 +197,9 @@ class OidcProvider {
160
197
  async buildProfile(tokenSet, includeUserInfo = true) {
161
198
  let claims = { ...tokenSet.claims };
162
199
  // Optionally fetch additional claims from UserInfo endpoint
163
- if (includeUserInfo && this.config.userinfoEndpoint) {
200
+ // Check both manual config and discovered endpoint (via OIDC discovery)
201
+ const userinfoUrl = this.config.userinfoEndpoint || this.issuer?.metadata?.userinfo_endpoint;
202
+ if (includeUserInfo && userinfoUrl) {
164
203
  try {
165
204
  const userinfo = await this.getUserInfo(tokenSet.accessToken);
166
205
  // Merge UserInfo claims with ID token claims
@@ -168,16 +207,18 @@ class OidcProvider {
168
207
  }
169
208
  catch (error) {
170
209
  // UserInfo is optional - continue with ID token claims only
171
- console.warn("Failed to fetch UserInfo, using ID token claims only:", error);
210
+ log_1.oidcLog.warn(`Failed to fetch UserInfo from ${userinfoUrl}, using ID token claims only:`, error);
172
211
  }
173
212
  }
174
213
  // Apply custom claim mapping if configured
175
214
  if (this.config.claimMapping) {
176
215
  const customClaims = (0, claims_mapper_1.extractCustomClaims)(claims, this.config.claimMapping);
177
216
  claims = { ...claims, ...customClaims };
217
+ log_1.oidcLog.trace(`Provider "${this.config.issuer}": applied custom claim mapping`, claims);
178
218
  }
179
219
  // Map to normalized profile
180
220
  const profile = (0, claims_mapper_1.mapClaimsToProfile)(claims);
221
+ log_1.oidcLog.trace(`Provider "${this.config.issuer}": built final profile`, profile);
181
222
  return profile;
182
223
  }
183
224
  /**
@@ -13,7 +13,8 @@ export declare class ProviderRegistry {
13
13
  private staticProviders;
14
14
  private providerInstances;
15
15
  private providerLoader?;
16
- constructor(staticProviders: Record<string, OidcProviderConfig>, providerLoader?: (providerName: string) => Promise<OidcProviderConfig | null>);
16
+ private ctx;
17
+ constructor(staticProviders: Record<string, OidcProviderConfig>, providerLoader?: (providerName: string, ctx: any) => Promise<OidcProviderConfig | null>, ctx?: any);
17
18
  /**
18
19
  * Get provider instance by name
19
20
  *
@@ -1 +1 @@
1
- {"version":3,"file":"ProviderRegistry.d.ts","sourceRoot":"","sources":["../../src/providers/ProviderRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAG3D;;;;;;;;GAQG;AACH,qBAAa,gBAAgB;IACzB,OAAO,CAAC,eAAe,CAAqC;IAC5D,OAAO,CAAC,iBAAiB,CAAwC;IACjE,OAAO,CAAC,cAAc,CAAC,CAA+D;gBAGlF,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,EACnD,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAMjF;;;;;;;;;;;;;OAaG;IACG,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAgC9D;;;;;OAKG;IACH,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAI1C;;;;;;;OAOG;IACH,UAAU,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI;IAQvC;;;;OAIG;IACH,gBAAgB,IAAI,MAAM,EAAE;CAG/B"}
1
+ {"version":3,"file":"ProviderRegistry.d.ts","sourceRoot":"","sources":["../../src/providers/ProviderRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAI3D;;;;;;;;GAQG;AACH,qBAAa,gBAAgB;IACzB,OAAO,CAAC,eAAe,CAAqC;IAC5D,OAAO,CAAC,iBAAiB,CAAwC;IACjE,OAAO,CAAC,cAAc,CAAC,CAAyE;IAChG,OAAO,CAAC,GAAG,CAAM;gBAGb,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,EACnD,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,EACvF,GAAG,CAAC,EAAE,GAAG;IAOb;;;;;;;;;;;;;OAaG;IACG,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IA+C9D;;;;;OAKG;IACH,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO;IAI1C;;;;;;;OAOG;IACH,UAAU,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI;IAQvC;;;;OAIG;IACH,gBAAgB,IAAI,MAAM,EAAE;CAG/B"}
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ProviderRegistry = void 0;
4
4
  const OidcProvider_1 = require("./OidcProvider");
5
5
  const error_utils_1 = require("../utils/error-utils");
6
+ const log_1 = require("../log");
6
7
  /**
7
8
  * Provider registry for managing OIDC provider instances
8
9
  *
@@ -13,10 +14,11 @@ const error_utils_1 = require("../utils/error-utils");
13
14
  * - Lazy initialization
14
15
  */
15
16
  class ProviderRegistry {
16
- constructor(staticProviders, providerLoader) {
17
+ constructor(staticProviders, providerLoader, ctx) {
17
18
  this.providerInstances = new Map();
18
19
  this.staticProviders = staticProviders;
19
20
  this.providerLoader = providerLoader;
21
+ this.ctx = ctx;
20
22
  }
21
23
  /**
22
24
  * Get provider instance by name
@@ -36,20 +38,26 @@ class ProviderRegistry {
36
38
  // Check cache first
37
39
  const cachedProvider = this.providerInstances.get(providerName);
38
40
  if (cachedProvider) {
41
+ log_1.oidcLog.debug(`Provider "${providerName}": using cached instance`);
39
42
  return cachedProvider;
40
43
  }
41
44
  // Try static configuration
42
45
  let config = this.staticProviders[providerName] || null;
46
+ let configSource = "static";
43
47
  // Try dynamic loader if not in static config
44
48
  if (!config && this.providerLoader) {
45
- config = await this.providerLoader(providerName);
49
+ log_1.oidcLog.debug(`Provider "${providerName}": not in static config, invoking providerLoader`);
50
+ config = await this.providerLoader(providerName, this.ctx);
51
+ configSource = "dynamic";
46
52
  }
47
53
  if (!config) {
54
+ log_1.oidcLog.debug(`Provider "${providerName}": not found (static=[${Object.keys(this.staticProviders).join(", ")}], loader=${!!this.providerLoader})`);
48
55
  throw (0, error_utils_1.createOidcError)(error_utils_1.OidcErrorCodes.PROVIDER_NOT_CONFIGURED, `OIDC provider '${providerName}' is not configured`, {
49
56
  providerName,
50
57
  availableProviders: Object.keys(this.staticProviders),
51
58
  });
52
59
  }
60
+ log_1.oidcLog.debug(`Provider "${providerName}" resolved via ${configSource}:`, `issuer=${config.issuer}`, `clientId=${config.clientId}`, `clientSecret=${(0, log_1.maskSecret)(config.clientSecret)}`, `callbackUrl=${config.callbackUrl}`, config.discoveryUrl ? `discoveryUrl=${config.discoveryUrl}` : `authorizationEndpoint=${config.authorizationEndpoint}`, `scope=${(config.scope || ["openid", "email", "profile"]).join(" ")}`);
53
61
  // Create and initialize provider
54
62
  const provider = new OidcProvider_1.OidcProvider(config);
55
63
  await provider.initialize();
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * GET /oidc/:provider/callback?code=...&state=...&response_type=json
5
5
  */
6
- export default interface CallbackRequest {
6
+ type CallbackRequest = Record<string, string | string[]> & {
7
7
  /**
8
8
  * Authorization code from the IdP
9
9
  * Required for successful authentication
@@ -29,9 +29,6 @@ export default interface CallbackRequest {
29
29
  * - undefined: Redirect to redirectUri with token in URL fragment
30
30
  */
31
31
  response_type?: "json";
32
- /**
33
- * Index signature for Flink Query type compatibility
34
- */
35
- [key: string]: string | string[] | undefined;
36
- }
32
+ };
33
+ export default CallbackRequest;
37
34
  //# sourceMappingURL=CallbackRequest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"CallbackRequest.d.ts","sourceRoot":"","sources":["../../src/schemas/CallbackRequest.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,OAAO,WAAW,eAAe;IACpC;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;CAChD"}
1
+ {"version":3,"file":"CallbackRequest.d.ts","sourceRoot":"","sources":["../../src/schemas/CallbackRequest.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,KAAK,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG;IACvD;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CAE1B,CAAC;AAEF,eAAe,eAAe,CAAC"}
@@ -3,15 +3,12 @@
3
3
  *
4
4
  * GET /oidc/:provider/initiate?redirectUri=...
5
5
  */
6
- export default interface InitiateRequest {
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
- /**
13
- * Index signature for Flink Query type compatibility
14
- */
15
- [key: string]: string | string[] | undefined;
16
- }
12
+ };
13
+ export default InitiateRequest;
17
14
  //# sourceMappingURL=InitiateRequest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"InitiateRequest.d.ts","sourceRoot":"","sources":["../../src/schemas/InitiateRequest.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,OAAO,WAAW,eAAe;IACpC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;CAChD"}
1
+ {"version":3,"file":"InitiateRequest.d.ts","sourceRoot":"","sources":["../../src/schemas/InitiateRequest.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,KAAK,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG;IACvD;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,eAAe,eAAe,CAAC"}
@@ -37,6 +37,11 @@ export default interface OidcSession {
37
37
  * Can be overridden by the client via query parameter
38
38
  */
39
39
  redirectUri: string;
40
+ /**
41
+ * Arbitrary metadata collected from meta.* query parameters on initiate
42
+ * Passed through to onAuthSuccess and onAuthError callbacks unchanged
43
+ */
44
+ metadata?: Record<string, string>;
40
45
  /**
41
46
  * Session creation timestamp
42
47
  * MongoDB TTL index will automatically delete expired sessions
@@ -1 +1 @@
1
- {"version":3,"file":"OidcSession.d.ts","sourceRoot":"","sources":["../../src/schemas/OidcSession.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,OAAO,WAAW,WAAW;IAChC;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,SAAS,EAAE,IAAI,CAAC;CACnB"}
1
+ {"version":3,"file":"OidcSession.d.ts","sourceRoot":"","sources":["../../src/schemas/OidcSession.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,OAAO,WAAW,WAAW;IAChC;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAElC;;;OAGG;IACH,SAAS,EAAE,IAAI,CAAC;CACnB"}
@@ -1,18 +1,29 @@
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";
1
14
  /**
2
15
  * Format token response for the client
3
16
  *
4
17
  * Supports two response formats:
5
18
  * 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.
19
+ * 2. Redirect with token in URL fragment or query (for web/native clients)
10
20
  *
11
21
  * @param token - JWT token for the application
12
22
  * @param user - User object
13
23
  * @param redirectUrl - URL to redirect to
14
24
  * @param responseType - "json" or undefined (redirect)
25
+ * @param tokenDelivery - How to attach the token on redirect. Defaults to "fragment".
15
26
  * @returns Flink response object
16
27
  */
17
- export declare function formatTokenResponse(token: string, user: any, redirectUrl: string, responseType?: "json"): any;
28
+ export declare function formatTokenResponse(token: string, user: any, redirectUrl: string, responseType?: "json", tokenDelivery?: TokenDeliveryMode): any;
18
29
  //# sourceMappingURL=response-utils.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"response-utils.d.ts","sourceRoot":"","sources":["../../src/utils/response-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,OAAO,CAAC;AAErD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAC/B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,GAAG,EACT,WAAW,EAAE,MAAM,EACnB,YAAY,CAAC,EAAE,MAAM,EACrB,aAAa,GAAE,iBAA8B,GAC9C,GAAG,CAqBL"}
@@ -6,18 +6,16 @@ exports.formatTokenResponse = formatTokenResponse;
6
6
  *
7
7
  * Supports two response formats:
8
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.
9
+ * 2. Redirect with token in URL fragment or query (for web/native clients)
13
10
  *
14
11
  * @param token - JWT token for the application
15
12
  * @param user - User object
16
13
  * @param redirectUrl - URL to redirect to
17
14
  * @param responseType - "json" or undefined (redirect)
15
+ * @param tokenDelivery - How to attach the token on redirect. Defaults to "fragment".
18
16
  * @returns Flink response object
19
17
  */
20
- function formatTokenResponse(token, user, redirectUrl, responseType) {
18
+ function formatTokenResponse(token, user, redirectUrl, responseType, tokenDelivery = "fragment") {
21
19
  // JSON response for API clients
22
20
  if (responseType === "json") {
23
21
  return {
@@ -27,11 +25,8 @@ function formatTokenResponse(token, user, redirectUrl, responseType) {
27
25
  },
28
26
  };
29
27
  }
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}`;
28
+ const tokenParam = `token=${encodeURIComponent(token)}`;
29
+ const finalUrl = appendToken(redirectUrl, tokenParam, tokenDelivery);
35
30
  return {
36
31
  status: 302,
37
32
  headers: {
@@ -40,3 +35,29 @@ function formatTokenResponse(token, user, redirectUrl, responseType) {
40
35
  data: {},
41
36
  };
42
37
  }
38
+ /**
39
+ * Append a token parameter to a URL using the specified delivery mode.
40
+ *
41
+ * For "query" mode, any existing `#fragment` on the URL is preserved: the
42
+ * query parameter is spliced in *before* the fragment so the final URL is
43
+ * still well-formed.
44
+ *
45
+ * For "fragment" mode, a second token on an existing fragment is joined
46
+ * with `&` (same as the old behavior).
47
+ */
48
+ function appendToken(redirectUrl, tokenParam, mode) {
49
+ if (mode === "query") {
50
+ const hashIdx = redirectUrl.indexOf("#");
51
+ if (hashIdx >= 0) {
52
+ const base = redirectUrl.slice(0, hashIdx);
53
+ const frag = redirectUrl.slice(hashIdx);
54
+ const sep = base.includes("?") ? "&" : "?";
55
+ return `${base}${sep}${tokenParam}${frag}`;
56
+ }
57
+ const sep = redirectUrl.includes("?") ? "&" : "?";
58
+ return `${redirectUrl}${sep}${tokenParam}`;
59
+ }
60
+ // fragment (default)
61
+ const sep = redirectUrl.includes("#") ? "&" : "#";
62
+ return `${redirectUrl}${sep}${tokenParam}`;
63
+ }
@@ -86,7 +86,7 @@ async function start() {
86
86
  provider,
87
87
  });
88
88
 
89
- await ctx.repos.userRepo.updateOne(user._id, {
89
+ await ctx.repos.userRepo.updateById(user._id, {
90
90
  oidcConnections: user.oidcConnections,
91
91
  });
92
92
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flink-app/oidc-plugin",
3
- "version": "1.0.0",
3
+ "version": "2.0.0-alpha.100",
4
4
  "description": "Flink plugin for OIDC authentication with generic IdP support",
5
5
  "author": "joel@frost.se",
6
6
  "license": "MIT",
@@ -10,19 +10,16 @@
10
10
  "access": "public"
11
11
  },
12
12
  "dependencies": {
13
- "openid-client": "^5.7.0"
13
+ "openid-client": "^5.7.0",
14
+ "@flink-app/jwt-auth-plugin": "2.0.0-alpha.100"
14
15
  },
15
16
  "peerDependencies": {
16
- "mongodb": "^6.15.0",
17
- "@flink-app/flink": "0.13.0",
18
- "@flink-app/jwt-auth-plugin": "0.13.0"
17
+ "@flink-app/flink": ">=2.0.0-alpha.100",
18
+ "mongodb": "^6.15.0"
19
19
  },
20
20
  "peerDependenciesMeta": {
21
21
  "@flink-app/flink": {
22
22
  "optional": false
23
- },
24
- "@flink-app/jwt-auth-plugin": {
25
- "optional": false
26
23
  }
27
24
  },
28
25
  "devDependencies": {
@@ -30,9 +27,9 @@
30
27
  "@types/node": "22.13.10",
31
28
  "ts-node": "^10.9.2",
32
29
  "tsc-watch": "^4.2.9",
33
- "@flink-app/flink": "0.13.0",
34
- "@flink-app/jwt-auth-plugin": "0.13.0",
35
- "@flink-app/test-utils": "0.13.0"
30
+ "@flink-app/flink": "2.0.0-alpha.100",
31
+ "@flink-app/jwt-auth-plugin": "2.0.0-alpha.100",
32
+ "@flink-app/test-utils": "2.0.0-alpha.100"
36
33
  },
37
34
  "scripts": {
38
35
  "test": "jasmine-ts --config=./spec/support/jasmine.json",
@@ -39,6 +39,20 @@ export function createTestProviderConfig(overrides?: Partial<OidcProviderConfig>
39
39
  };
40
40
  }
41
41
 
42
+ /**
43
+ * Create a test OIDC provider configuration for a public client (no clientSecret)
44
+ */
45
+ export function createPublicClientProviderConfig(overrides?: Partial<OidcProviderConfig>): OidcProviderConfig {
46
+ return {
47
+ issuer: "https://test-idp.example.com",
48
+ clientId: "test-public-client-id",
49
+ callbackUrl: "http://localhost:3000/oidc/test/callback",
50
+ discoveryUrl: "https://test-idp.example.com/.well-known/openid-configuration",
51
+ scope: ["openid", "email", "profile"],
52
+ ...overrides,
53
+ };
54
+ }
55
+
42
56
  /**
43
57
  * Create a mock OIDC token set
44
58
  */
@@ -6,18 +6,45 @@
6
6
  */
7
7
 
8
8
  import { oidcPlugin } from "../../src/OidcPlugin";
9
- import { createTestProviderConfig } from "../helpers/test-helpers";
9
+ import { createTestProviderConfig, createPublicClientProviderConfig } from "../helpers/test-helpers";
10
10
 
11
11
  describe("OidcPlugin", () => {
12
12
 
13
13
  describe("configuration validation", () => {
14
- it("should throw error if no providers configured", () => {
14
+ it("should throw error if no providers and no providerLoader configured", () => {
15
15
  expect(() => {
16
16
  oidcPlugin({
17
17
  providers: {},
18
18
  onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
19
19
  });
20
- }).toThrowError(/At least one provider must be configured/);
20
+ }).toThrowError(/At least one provider must be configured, or providerLoader must be provided/);
21
+ });
22
+
23
+ it("should throw error if providers omitted and no providerLoader configured", () => {
24
+ expect(() => {
25
+ oidcPlugin({
26
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
27
+ });
28
+ }).toThrowError(/At least one provider must be configured, or providerLoader must be provided/);
29
+ });
30
+
31
+ it("should accept empty providers when providerLoader is configured", () => {
32
+ expect(() => {
33
+ oidcPlugin({
34
+ providers: {},
35
+ providerLoader: async () => null,
36
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
37
+ });
38
+ }).not.toThrow();
39
+ });
40
+
41
+ it("should accept omitted providers when providerLoader is configured", () => {
42
+ expect(() => {
43
+ oidcPlugin({
44
+ providerLoader: async () => null,
45
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
46
+ });
47
+ }).not.toThrow();
21
48
  });
22
49
 
23
50
  it("should throw error if onAuthSuccess is missing", () => {
@@ -72,6 +99,80 @@ describe("OidcPlugin", () => {
72
99
  });
73
100
  });
74
101
 
102
+ describe("public client (PKCE-only) support", () => {
103
+ it("should accept provider without clientSecret when encryptionKey is provided", () => {
104
+ expect(() => {
105
+ oidcPlugin({
106
+ providers: {
107
+ test: createPublicClientProviderConfig(),
108
+ },
109
+ encryptionKey: "valid-encryption-key-at-least-32-chars-long",
110
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
111
+ });
112
+ }).not.toThrow();
113
+ });
114
+
115
+ it("should accept provider without clientSecret when storeTokens is false", () => {
116
+ expect(() => {
117
+ oidcPlugin({
118
+ providers: {
119
+ test: createPublicClientProviderConfig(),
120
+ },
121
+ storeTokens: false,
122
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
123
+ });
124
+ }).not.toThrow();
125
+ });
126
+
127
+ it("should accept provider without clientSecret when storeTokens is not set (defaults to false)", () => {
128
+ expect(() => {
129
+ oidcPlugin({
130
+ providers: {
131
+ test: createPublicClientProviderConfig(),
132
+ },
133
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
134
+ });
135
+ }).not.toThrow();
136
+ });
137
+
138
+ it("should throw when storeTokens is true, no encryptionKey, and no provider has clientSecret", () => {
139
+ expect(() => {
140
+ oidcPlugin({
141
+ providers: {
142
+ test: createPublicClientProviderConfig(),
143
+ },
144
+ storeTokens: true,
145
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
146
+ });
147
+ }).toThrowError(/Encryption key must be at least 32 characters/);
148
+ });
149
+
150
+ it("should derive encryption key from provider with clientSecret when mixed with public clients", () => {
151
+ expect(() => {
152
+ oidcPlugin({
153
+ providers: {
154
+ publicProvider: createPublicClientProviderConfig(),
155
+ confidentialProvider: createTestProviderConfig(),
156
+ },
157
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
158
+ });
159
+ }).not.toThrow();
160
+ });
161
+
162
+ it("should accept storeTokens with explicit encryptionKey for public clients", () => {
163
+ expect(() => {
164
+ oidcPlugin({
165
+ providers: {
166
+ test: createPublicClientProviderConfig(),
167
+ },
168
+ storeTokens: true,
169
+ encryptionKey: "valid-encryption-key-at-least-32-chars-long",
170
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
171
+ });
172
+ }).not.toThrow();
173
+ });
174
+ });
175
+
75
176
  describe("plugin structure", () => {
76
177
  it("should return plugin with correct ID", () => {
77
178
  const plugin = oidcPlugin({
@@ -29,7 +29,7 @@ describe("ProviderRegistry", () => {
29
29
  });
30
30
 
31
31
  it("should create registry with provider loader", () => {
32
- const loader = async (name: string) => null;
32
+ const loader = async (name: string, ctx: any) => null;
33
33
  const registry = new ProviderRegistry(staticProviders, loader);
34
34
  expect(registry).toBeDefined();
35
35
  });
@@ -49,7 +49,7 @@ describe("ProviderRegistry", () => {
49
49
  });
50
50
 
51
51
  it("should return true if provider loader is configured", () => {
52
- const loader = async (name: string) => null;
52
+ const loader = async (name: string, ctx: any) => null;
53
53
  const registry = new ProviderRegistry(staticProviders, loader);
54
54
  // hasProvider returns true if loader exists, even for unknown providers
55
55
  expect(registry.hasProvider("unknown")).toBe(true);
@@ -87,7 +87,8 @@ describe("ProviderRegistry", () => {
87
87
  });
88
88
 
89
89
  const loader = jasmine.createSpy("loader").and.returnValue(Promise.resolve(dynamicConfig));
90
- const registry = new ProviderRegistry(staticProviders, loader);
90
+ const mockCtx = { repos: {} };
91
+ const registry = new ProviderRegistry(staticProviders, loader, mockCtx);
91
92
 
92
93
  // Note: This will attempt real OIDC discovery, which will fail in tests
93
94
  // We're primarily testing that the loader is called
@@ -95,13 +96,13 @@ describe("ProviderRegistry", () => {
95
96
  await registry.getProvider("okta");
96
97
  } catch (error) {
97
98
  // Discovery will fail, but loader should have been called
98
- expect(loader).toHaveBeenCalledWith("okta");
99
+ expect(loader).toHaveBeenCalledWith("okta", mockCtx);
99
100
  }
100
101
  });
101
102
 
102
103
  it("should prioritize static config over loader", async () => {
103
104
  const loader = jasmine.createSpy("loader").and.returnValue(Promise.resolve(null));
104
- const registry = new ProviderRegistry(staticProviders, loader);
105
+ const registry = new ProviderRegistry(staticProviders, loader, {});
105
106
 
106
107
  // Note: This will attempt real OIDC discovery
107
108
  try {
@@ -113,7 +114,7 @@ describe("ProviderRegistry", () => {
113
114
  });
114
115
 
115
116
  it("should throw error if loader returns null", async () => {
116
- const loader = async (name: string) => null;
117
+ const loader = async (name: string, ctx: any) => null;
117
118
  const registry = new ProviderRegistry({}, loader);
118
119
 
119
120
  try {
@@ -169,7 +170,7 @@ describe("ProviderRegistry", () => {
169
170
  })
170
171
  )
171
172
  );
172
- const registry = new ProviderRegistry({}, loader);
173
+ const registry = new ProviderRegistry({}, loader, {});
173
174
 
174
175
  // First call - will attempt to initialize (and fail on discovery)
175
176
  try {