@flink-app/oidc-plugin 2.0.0-alpha.67 → 2.0.0-alpha.69

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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # @flink-app/oidc-plugin
2
2
 
3
+ ## 2.0.0-alpha.69
4
+
5
+ ### Minor Changes
6
+
7
+ - feat(oidc-plugin): support public clients with PKCE-only authentication (no client secret)
8
+
9
+ `clientSecret` is now optional in `OidcProviderConfig`. When omitted, the plugin configures `openid-client` with `token_endpoint_auth_method: "none"` for public client flows. Added `tokenEndpointAuthMethod` option for explicit control. Encryption key derivation skips providers without a secret, and `storeTokens: true` requires an explicit `encryptionKey` when no provider has a client secret.
10
+
11
+ ### Patch Changes
12
+
13
+ - @flink-app/flink@2.0.0-alpha.69
14
+ - @flink-app/jwt-auth-plugin@2.0.0-alpha.69
15
+
16
+ ## 2.0.0-alpha.68
17
+
18
+ ### Patch Changes
19
+
20
+ - @flink-app/flink@2.0.0-alpha.68
21
+ - @flink-app/jwt-auth-plugin@2.0.0-alpha.68
22
+
3
23
  ## 2.0.0-alpha.67
4
24
 
5
25
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"OidcPlugin.d.ts","sourceRoot":"","sources":["../src/OidcPlugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,WAAW,EAAO,MAAM,kBAAkB,CAAC;AAE9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAWxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,WAAW,CA2MlE"}
1
+ {"version":3,"file":"OidcPlugin.d.ts","sourceRoot":"","sources":["../src/OidcPlugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,WAAW,EAAO,MAAM,kBAAkB,CAAC;AAE9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAWxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,WAAW,CAiNlE"}
@@ -124,9 +124,6 @@ function oidcPlugin(options) {
124
124
  if (!providerConfig.clientId) {
125
125
  throw new Error(`OIDC Plugin: ${providerName} clientId is required`);
126
126
  }
127
- if (!providerConfig.clientSecret) {
128
- throw new Error(`OIDC Plugin: ${providerName} clientSecret is required`);
129
- }
130
127
  if (!providerConfig.callbackUrl) {
131
128
  throw new Error(`OIDC Plugin: ${providerName} callbackUrl is required`);
132
129
  }
@@ -144,19 +141,28 @@ function oidcPlugin(options) {
144
141
  // Determine encryption key
145
142
  let encryptionKey = options.encryptionKey;
146
143
  if (!encryptionKey) {
147
- // Derive from first configured provider's client secret
148
- const firstProvider = configuredProviders[0];
149
- const firstProviderConfig = options.providers[firstProvider];
150
- if (firstProviderConfig) {
151
- encryptionKey = firstProviderConfig.clientSecret;
144
+ // Derive from the first configured provider that has a client secret
145
+ const providerWithSecret = configuredProviders.find((name) => options.providers[name]?.clientSecret);
146
+ if (providerWithSecret) {
147
+ encryptionKey = options.providers[providerWithSecret].clientSecret;
152
148
  flink_1.log.warn("OIDC Plugin: No encryption key provided, deriving from client secret. " + "For better security, provide a dedicated encryptionKey in options.");
153
149
  }
154
150
  }
155
- if (!encryptionKey || encryptionKey.length < 32) {
156
- throw new Error("OIDC Plugin: Encryption key must be at least 32 characters");
151
+ // Encryption key is required when storing tokens
152
+ if (options.storeTokens) {
153
+ if (!encryptionKey || encryptionKey.length < 32) {
154
+ throw new Error("OIDC Plugin: Encryption key must be at least 32 characters. " +
155
+ "Provide an explicit encryptionKey when using storeTokens with public clients (no clientSecret).");
156
+ }
157
+ (0, encryption_utils_1.validateEncryptionSecret)(encryptionKey);
158
+ }
159
+ else if (encryptionKey) {
160
+ // Validate if provided, even when not storing tokens (used for getConnection/getConnections)
161
+ if (encryptionKey.length < 32) {
162
+ throw new Error("OIDC Plugin: Encryption key must be at least 32 characters");
163
+ }
164
+ (0, encryption_utils_1.validateEncryptionSecret)(encryptionKey);
157
165
  }
158
- // Validate encryption key
159
- (0, encryption_utils_1.validateEncryptionSecret)(encryptionKey);
160
166
  let flinkApp;
161
167
  let sessionRepo;
162
168
  let connectionRepo;
@@ -246,7 +246,9 @@ export interface OidcPluginOptions {
246
246
  sessionTTL?: number;
247
247
  /**
248
248
  * Encryption key for encrypting stored OIDC tokens
249
- * If not provided, will be derived from first configured provider's client secret
249
+ * If not provided, will be derived from the first configured provider's client secret
250
+ *
251
+ * Required when using storeTokens with public clients (no clientSecret).
250
252
  *
251
253
  * Recommended: Use a dedicated encryption key from environment variables
252
254
  * Must be at least 32 characters
@@ -1 +1 @@
1
- {"version":3,"file":"OidcPluginOptions.d.ts","sourceRoot":"","sources":["../src/OidcPluginOptions.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,uBAAuB,CAAC;AAChD,OAAO,YAAY,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,SAAS;IACtB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,OAAO,CAAC,EAAE,GAAG,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,2BAA2B;IACxC;;;OAGG;IACH,IAAI,EAAE,GAAG,CAAC;IAEV;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACtC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAC9B;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAE9C;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgDG;IACH,aAAa,EAAE,CACX,MAAM,EAAE;QACJ;;WAEG;QACH,OAAO,EAAE,WAAW,CAAC;QAErB;;;WAGG;QACH,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAE5B;;WAEG;QACH,QAAQ,EAAE,MAAM,CAAC;QAEjB;;;WAGG;QACH,MAAM,CAAC,EAAE,YAAY,CAAC;KACzB,EACD,GAAG,EAAE,GAAG,KACP,OAAO,CAAC,2BAA2B,CAAC,CAAC;IAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,SAAS,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,yBAAyB,CAAC,CAAC;IAErG;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IAE9E;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC;;;OAGG;IACH,yBAAyB,CAAC,EAAE,MAAM,CAAC;IAEnC;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC5B"}
1
+ {"version":3,"file":"OidcPluginOptions.d.ts","sourceRoot":"","sources":["../src/OidcPluginOptions.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,uBAAuB,CAAC;AAChD,OAAO,YAAY,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,SAAS;IACtB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,OAAO,CAAC,EAAE,GAAG,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,2BAA2B;IACxC;;;OAGG;IACH,IAAI,EAAE,GAAG,CAAC;IAEV;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACtC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAC9B;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAE9C;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgDG;IACH,aAAa,EAAE,CACX,MAAM,EAAE;QACJ;;WAEG;QACH,OAAO,EAAE,WAAW,CAAC;QAErB;;;WAGG;QACH,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAE5B;;WAEG;QACH,QAAQ,EAAE,MAAM,CAAC;QAEjB;;;WAGG;QACH,MAAM,CAAC,EAAE,YAAY,CAAC;KACzB,EACD,GAAG,EAAE,GAAG,KACP,OAAO,CAAC,2BAA2B,CAAC,CAAC;IAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,SAAS,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,yBAAyB,CAAC,CAAC;IAErG;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IAE9E;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC;;;OAGG;IACH,yBAAyB,CAAC,EAAE,MAAM,CAAC;IAEnC;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC5B"}
@@ -18,8 +18,19 @@ export interface OidcProviderConfig {
18
18
  /**
19
19
  * OAuth 2.0 client secret
20
20
  * Provided by the IdP - keep this secure!
21
+ *
22
+ * Optional for public clients using PKCE-only authentication.
23
+ * When omitted, token_endpoint_auth_method defaults to "none".
21
24
  */
22
- clientSecret: string;
25
+ clientSecret?: string;
26
+ /**
27
+ * Token endpoint authentication method
28
+ *
29
+ * - "client_secret_basic": HTTP Basic auth with client_id and client_secret (default when clientSecret is provided)
30
+ * - "client_secret_post": client_id and client_secret in POST body
31
+ * - "none": No client authentication (default when clientSecret is omitted, for public/PKCE-only clients)
32
+ */
33
+ tokenEndpointAuthMethod?: "client_secret_basic" | "client_secret_post" | "none";
23
34
  /**
24
35
  * Callback URL for OAuth redirect
25
36
  * Must match the redirect URI registered with the IdP
@@ -1 +1 @@
1
- {"version":3,"file":"OidcProviderConfig.d.ts","sourceRoot":"","sources":["../src/OidcProviderConfig.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IAC/B;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IAEjB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IAEH;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC"}
1
+ {"version":3,"file":"OidcProviderConfig.d.ts","sourceRoot":"","sources":["../src/OidcProviderConfig.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IAC/B;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;;OAMG;IACH,uBAAuB,CAAC,EAAE,qBAAqB,GAAG,oBAAoB,GAAG,MAAM,CAAC;IAEhF;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IAEjB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IAEH;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC"}
@@ -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;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;IA0DjC;;;;;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"}
@@ -54,12 +54,22 @@ class OidcProvider {
54
54
  });
55
55
  }
56
56
  // Create OIDC client
57
- this.client = new this.issuer.Client({
57
+ const clientMetadata = {
58
58
  client_id: this.config.clientId,
59
- client_secret: this.config.clientSecret,
60
59
  redirect_uris: [this.config.callbackUrl],
61
60
  response_types: ["code"],
62
- });
61
+ };
62
+ if (this.config.clientSecret) {
63
+ clientMetadata.client_secret = this.config.clientSecret;
64
+ clientMetadata.token_endpoint_auth_method =
65
+ this.config.tokenEndpointAuthMethod || "client_secret_basic";
66
+ }
67
+ else {
68
+ // Public client (PKCE-only) — openid-client requires this to skip client authentication
69
+ clientMetadata.token_endpoint_auth_method =
70
+ this.config.tokenEndpointAuthMethod || "none";
71
+ }
72
+ this.client = new this.issuer.Client(clientMetadata);
63
73
  this.initialized = true;
64
74
  }
65
75
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flink-app/oidc-plugin",
3
- "version": "2.0.0-alpha.67",
3
+ "version": "2.0.0-alpha.69",
4
4
  "description": "Flink plugin for OIDC authentication with generic IdP support",
5
5
  "author": "joel@frost.se",
6
6
  "license": "MIT",
@@ -11,10 +11,10 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "openid-client": "^5.7.0",
14
- "@flink-app/jwt-auth-plugin": "2.0.0-alpha.67"
14
+ "@flink-app/jwt-auth-plugin": "2.0.0-alpha.69"
15
15
  },
16
16
  "peerDependencies": {
17
- "@flink-app/flink": ">=2.0.0-alpha.67",
17
+ "@flink-app/flink": ">=2.0.0-alpha.69",
18
18
  "mongodb": "^6.15.0"
19
19
  },
20
20
  "peerDependenciesMeta": {
@@ -27,9 +27,9 @@
27
27
  "@types/node": "22.13.10",
28
28
  "ts-node": "^10.9.2",
29
29
  "tsc-watch": "^4.2.9",
30
- "@flink-app/test-utils": "2.0.0-alpha.67",
31
- "@flink-app/jwt-auth-plugin": "2.0.0-alpha.67",
32
- "@flink-app/flink": "2.0.0-alpha.67"
30
+ "@flink-app/flink": "2.0.0-alpha.69",
31
+ "@flink-app/test-utils": "2.0.0-alpha.69",
32
+ "@flink-app/jwt-auth-plugin": "2.0.0-alpha.69"
33
33
  },
34
34
  "scripts": {
35
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,7 +6,7 @@
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
 
@@ -72,6 +72,80 @@ describe("OidcPlugin", () => {
72
72
  });
73
73
  });
74
74
 
75
+ describe("public client (PKCE-only) support", () => {
76
+ it("should accept provider without clientSecret when encryptionKey is provided", () => {
77
+ expect(() => {
78
+ oidcPlugin({
79
+ providers: {
80
+ test: createPublicClientProviderConfig(),
81
+ },
82
+ encryptionKey: "valid-encryption-key-at-least-32-chars-long",
83
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
84
+ });
85
+ }).not.toThrow();
86
+ });
87
+
88
+ it("should accept provider without clientSecret when storeTokens is false", () => {
89
+ expect(() => {
90
+ oidcPlugin({
91
+ providers: {
92
+ test: createPublicClientProviderConfig(),
93
+ },
94
+ storeTokens: false,
95
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
96
+ });
97
+ }).not.toThrow();
98
+ });
99
+
100
+ it("should accept provider without clientSecret when storeTokens is not set (defaults to false)", () => {
101
+ expect(() => {
102
+ oidcPlugin({
103
+ providers: {
104
+ test: createPublicClientProviderConfig(),
105
+ },
106
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
107
+ });
108
+ }).not.toThrow();
109
+ });
110
+
111
+ it("should throw when storeTokens is true, no encryptionKey, and no provider has clientSecret", () => {
112
+ expect(() => {
113
+ oidcPlugin({
114
+ providers: {
115
+ test: createPublicClientProviderConfig(),
116
+ },
117
+ storeTokens: true,
118
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
119
+ });
120
+ }).toThrowError(/Encryption key must be at least 32 characters/);
121
+ });
122
+
123
+ it("should derive encryption key from provider with clientSecret when mixed with public clients", () => {
124
+ expect(() => {
125
+ oidcPlugin({
126
+ providers: {
127
+ publicProvider: createPublicClientProviderConfig(),
128
+ confidentialProvider: createTestProviderConfig(),
129
+ },
130
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
131
+ });
132
+ }).not.toThrow();
133
+ });
134
+
135
+ it("should accept storeTokens with explicit encryptionKey for public clients", () => {
136
+ expect(() => {
137
+ oidcPlugin({
138
+ providers: {
139
+ test: createPublicClientProviderConfig(),
140
+ },
141
+ storeTokens: true,
142
+ encryptionKey: "valid-encryption-key-at-least-32-chars-long",
143
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
144
+ });
145
+ }).not.toThrow();
146
+ });
147
+ });
148
+
75
149
  describe("plugin structure", () => {
76
150
  it("should return plugin with correct ID", () => {
77
151
  const plugin = oidcPlugin({
package/src/OidcPlugin.ts CHANGED
@@ -102,9 +102,6 @@ export function oidcPlugin(options: OidcPluginOptions): FlinkPlugin {
102
102
  if (!providerConfig.clientId) {
103
103
  throw new Error(`OIDC Plugin: ${providerName} clientId is required`);
104
104
  }
105
- if (!providerConfig.clientSecret) {
106
- throw new Error(`OIDC Plugin: ${providerName} clientSecret is required`);
107
- }
108
105
  if (!providerConfig.callbackUrl) {
109
106
  throw new Error(`OIDC Plugin: ${providerName} callbackUrl is required`);
110
107
  }
@@ -127,24 +124,33 @@ export function oidcPlugin(options: OidcPluginOptions): FlinkPlugin {
127
124
  // Determine encryption key
128
125
  let encryptionKey = options.encryptionKey;
129
126
  if (!encryptionKey) {
130
- // Derive from first configured provider's client secret
131
- const firstProvider = configuredProviders[0];
132
- const firstProviderConfig = options.providers[firstProvider];
133
- if (firstProviderConfig) {
134
- encryptionKey = firstProviderConfig.clientSecret;
127
+ // Derive from the first configured provider that has a client secret
128
+ const providerWithSecret = configuredProviders.find((name) => options.providers[name]?.clientSecret);
129
+ if (providerWithSecret) {
130
+ encryptionKey = options.providers[providerWithSecret].clientSecret;
135
131
  log.warn(
136
132
  "OIDC Plugin: No encryption key provided, deriving from client secret. " + "For better security, provide a dedicated encryptionKey in options."
137
133
  );
138
134
  }
139
135
  }
140
136
 
141
- if (!encryptionKey || encryptionKey.length < 32) {
142
- throw new Error("OIDC Plugin: Encryption key must be at least 32 characters");
137
+ // Encryption key is required when storing tokens
138
+ if (options.storeTokens) {
139
+ if (!encryptionKey || encryptionKey.length < 32) {
140
+ throw new Error(
141
+ "OIDC Plugin: Encryption key must be at least 32 characters. " +
142
+ "Provide an explicit encryptionKey when using storeTokens with public clients (no clientSecret)."
143
+ );
144
+ }
145
+ validateEncryptionSecret(encryptionKey);
146
+ } else if (encryptionKey) {
147
+ // Validate if provided, even when not storing tokens (used for getConnection/getConnections)
148
+ if (encryptionKey.length < 32) {
149
+ throw new Error("OIDC Plugin: Encryption key must be at least 32 characters");
150
+ }
151
+ validateEncryptionSecret(encryptionKey);
143
152
  }
144
153
 
145
- // Validate encryption key
146
- validateEncryptionSecret(encryptionKey);
147
-
148
154
  let flinkApp: FlinkApp<OidcInternalContext>;
149
155
  let sessionRepo: OidcSessionRepo;
150
156
  let connectionRepo: OidcConnectionRepo;
@@ -265,7 +265,9 @@ export interface OidcPluginOptions {
265
265
 
266
266
  /**
267
267
  * Encryption key for encrypting stored OIDC tokens
268
- * If not provided, will be derived from first configured provider's client secret
268
+ * If not provided, will be derived from the first configured provider's client secret
269
+ *
270
+ * Required when using storeTokens with public clients (no clientSecret).
269
271
  *
270
272
  * Recommended: Use a dedicated encryption key from environment variables
271
273
  * Must be at least 32 characters
@@ -20,8 +20,20 @@ export interface OidcProviderConfig {
20
20
  /**
21
21
  * OAuth 2.0 client secret
22
22
  * Provided by the IdP - keep this secure!
23
+ *
24
+ * Optional for public clients using PKCE-only authentication.
25
+ * When omitted, token_endpoint_auth_method defaults to "none".
23
26
  */
24
- clientSecret: string;
27
+ clientSecret?: string;
28
+
29
+ /**
30
+ * Token endpoint authentication method
31
+ *
32
+ * - "client_secret_basic": HTTP Basic auth with client_id and client_secret (default when clientSecret is provided)
33
+ * - "client_secret_post": client_id and client_secret in POST body
34
+ * - "none": No client authentication (default when clientSecret is omitted, for public/PKCE-only clients)
35
+ */
36
+ tokenEndpointAuthMethod?: "client_secret_basic" | "client_secret_post" | "none";
25
37
 
26
38
  /**
27
39
  * Callback URL for OAuth redirect
@@ -1,4 +1,4 @@
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";
@@ -65,12 +65,23 @@ export class OidcProvider {
65
65
  }
66
66
 
67
67
  // Create OIDC client
68
- this.client = new this.issuer.Client({
68
+ const clientMetadata: ClientMetadata = {
69
69
  client_id: this.config.clientId,
70
- client_secret: this.config.clientSecret,
71
70
  redirect_uris: [this.config.callbackUrl],
72
71
  response_types: ["code"],
73
- });
72
+ };
73
+
74
+ if (this.config.clientSecret) {
75
+ clientMetadata.client_secret = this.config.clientSecret;
76
+ clientMetadata.token_endpoint_auth_method =
77
+ this.config.tokenEndpointAuthMethod || "client_secret_basic";
78
+ } else {
79
+ // Public client (PKCE-only) — openid-client requires this to skip client authentication
80
+ clientMetadata.token_endpoint_auth_method =
81
+ this.config.tokenEndpointAuthMethod || "none";
82
+ }
83
+
84
+ this.client = new this.issuer.Client(clientMetadata);
74
85
 
75
86
  this.initialized = true;
76
87
  } catch (error: any) {