@incorta/auth 1.7.2 → 1.9.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/README.md CHANGED
@@ -4,6 +4,11 @@ Authenticate Incorta users in any Node.js app via Incorta's built-in OAuth
4
4
  2.0/OIDC authorization server. See the repository root README for the full
5
5
  guide; this is the API surface.
6
6
 
7
+ To *read* Incorta with the session this package holds — schemas, tables,
8
+ columns, rows, all filtered to the signed-in user — add
9
+ [`@incorta/sdk`](../sdk/README.md), released from this repository at the same
10
+ version.
11
+
7
12
  ## Entry points
8
13
 
9
14
  | Import | What's in it |
@@ -20,9 +25,12 @@ Returns `{ handler, getSession, config }`:
20
25
  - `handler(request: Request): Promise<Response>` — serves, under
21
26
  `config.basePath` (default `/auth`):
22
27
  - `GET /login?redirect_to=` — 302 into the Incorta authorize flow (or straight
23
- back when already signed in). Sets a sealed state+nonce cookie (10 min).
24
- - `GET /callback` — validates state, exchanges the code
25
- (`client_secret_basic`), verifies the ID token against the tenant JWKS
28
+ back when already signed in), with a PKCE S256 challenge. Sets a sealed
29
+ state+nonce+verifier cookie (10 min).
30
+ - `GET /callback` — validates state, exchanges the code with the PKCE
31
+ verifier (as a public client — `client_id` in the body — or with
32
+ `client_secret_basic` when a secret is configured), verifies the ID token
33
+ against the tenant JWKS
26
34
  (issuer, audience, nonce), seals the session cookie, 302 to the original page.
27
35
  Failures 302 to `{redirect_to}?incorta_auth_error={code}`.
28
36
  - `GET /session` — `{ user, session }` or `{ user: null }`. Refreshes the
@@ -39,7 +47,9 @@ Returns `{ handler, getSession, config }`:
39
47
  for server code (`user`, `accessToken`, `accessTokenExpiresAt`, `expiresAt`).
40
48
 
41
49
  Config falls back to `INCORTA_URL`, `INCORTA_TENANT`, `INCORTA_CLIENT_ID`,
42
- `INCORTA_CLIENT_SECRET`, `INCORTA_AUTH_SECRET`, `INCORTA_APP_URL`, and
50
+ `INCORTA_AUTH_SECRET`, `INCORTA_APP_URL`, `INCORTA_CLIENT_SECRET` (optional —
51
+ only a confidential client has one; Incorta registers apps as public
52
+ clients), and
43
53
  `INCORTA_INTERNAL_URL` (split horizon: server-side discovery/token/JWKS calls
44
54
  use it when the public `incortaUrl` is not routable from the app — e.g.
45
55
  `http://host.docker.internal:8080/incorta` from a local container; the
@@ -50,7 +50,13 @@ interface IncortaAuthConfig {
50
50
  tenant?: string;
51
51
  /** OAuth client id (see `incorta-auth register`). Defaults to `INCORTA_CLIENT_ID`. */
52
52
  clientId?: string;
53
- /** OAuth client secret. Defaults to `INCORTA_CLIENT_SECRET`. */
53
+ /**
54
+ * OAuth client secret — only for a CONFIDENTIAL client. Incorta's dynamic
55
+ * registration issues app clients as PUBLIC clients (no secret; see
56
+ * `registerClient`), and the SDK then authenticates token requests with
57
+ * `client_id` alone and relies on PKCE. Defaults to `INCORTA_CLIENT_SECRET`;
58
+ * unset or empty means public.
59
+ */
54
60
  clientSecret?: string;
55
61
  /**
56
62
  * Secret used to encrypt the session cookie (any long random string, >= 32 chars).
@@ -157,7 +163,8 @@ interface ResolvedConfig {
157
163
  internalIncortaUrl?: string;
158
164
  tenant: string;
159
165
  clientId: string;
160
- clientSecret: string;
166
+ /** Absent for a public client: token requests then carry `client_id` instead of Basic auth. */
167
+ clientSecret?: string;
161
168
  secret: string;
162
169
  basePath: string;
163
170
  appUrl?: string;
@@ -50,7 +50,13 @@ interface IncortaAuthConfig {
50
50
  tenant?: string;
51
51
  /** OAuth client id (see `incorta-auth register`). Defaults to `INCORTA_CLIENT_ID`. */
52
52
  clientId?: string;
53
- /** OAuth client secret. Defaults to `INCORTA_CLIENT_SECRET`. */
53
+ /**
54
+ * OAuth client secret — only for a CONFIDENTIAL client. Incorta's dynamic
55
+ * registration issues app clients as PUBLIC clients (no secret; see
56
+ * `registerClient`), and the SDK then authenticates token requests with
57
+ * `client_id` alone and relies on PKCE. Defaults to `INCORTA_CLIENT_SECRET`;
58
+ * unset or empty means public.
59
+ */
54
60
  clientSecret?: string;
55
61
  /**
56
62
  * Secret used to encrypt the session cookie (any long random string, >= 32 chars).
@@ -157,7 +163,8 @@ interface ResolvedConfig {
157
163
  internalIncortaUrl?: string;
158
164
  tenant: string;
159
165
  clientId: string;
160
- clientSecret: string;
166
+ /** Absent for a public client: token requests then carry `client_id` instead of Basic auth. */
167
+ clientSecret?: string;
161
168
  secret: string;
162
169
  basePath: string;
163
170
  appUrl?: string;
package/dist/cli.js CHANGED
@@ -49,14 +49,13 @@ async function registerClient(options) {
49
49
  })
50
50
  });
51
51
  const body = await readJson(response, "Client registration");
52
- if (!body.client_id || !body.client_secret) {
53
- throw new Error(
54
- `[incorta-auth] Client registration failed: response is missing client_id/client_secret.`
55
- );
52
+ if (!body.client_id) {
53
+ throw new Error(`[incorta-auth] Client registration failed: response is missing client_id.`);
56
54
  }
57
55
  return {
58
56
  clientId: body.client_id,
59
- clientSecret: body.client_secret,
57
+ clientSecret: body.client_secret || void 0,
58
+ tokenEndpointAuthMethod: body.token_endpoint_auth_method,
60
59
  registrationAccessToken: body.registration_access_token,
61
60
  registrationClientUri: body.registration_client_uri
62
61
  };
@@ -237,7 +236,13 @@ async function runRegister(args) {
237
236
  console.log(`INCORTA_URL=${args.url}`);
238
237
  console.log(`INCORTA_TENANT=${args.tenant}`);
239
238
  console.log(`INCORTA_CLIENT_ID=${client.clientId}`);
240
- console.log(`INCORTA_CLIENT_SECRET=${client.clientSecret}`);
239
+ if (client.clientSecret) {
240
+ console.log(`INCORTA_CLIENT_SECRET=${client.clientSecret}`);
241
+ } else {
242
+ console.log(
243
+ `# Public client (${client.tokenEndpointAuthMethod ?? "none"}): Incorta issued no client secret; PKCE protects the code flow.`
244
+ );
245
+ }
241
246
  console.log(`INCORTA_AUTH_SECRET=${authSecret}`);
242
247
  if (client.registrationAccessToken) {
243
248
  console.log(
@@ -1,6 +1,6 @@
1
1
  import { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import { AuthSession, IncortaAuth } from './index.cjs';
3
- export { A as AppAccess, a as AppAccessPolicy } from './app-access-BSsKtvbi.cjs';
3
+ export { A as AppAccess, a as AppAccessPolicy } from './app-access-B5lrn34s.cjs';
4
4
  import 'jose';
5
5
 
6
6
  /**
package/dist/express.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import { AuthSession, IncortaAuth } from './index.js';
3
- export { A as AppAccess, a as AppAccessPolicy } from './app-access-BSsKtvbi.js';
3
+ export { A as AppAccess, a as AppAccessPolicy } from './app-access-B5lrn34s.js';
4
4
  import 'jose';
5
5
 
6
6
  /**
package/dist/index.cjs CHANGED
@@ -68,11 +68,7 @@ function resolveConfig(input = {}) {
68
68
  "clientId",
69
69
  "INCORTA_CLIENT_ID"
70
70
  );
71
- const clientSecret = required(
72
- input.clientSecret ?? env.INCORTA_CLIENT_SECRET,
73
- "clientSecret",
74
- "INCORTA_CLIENT_SECRET"
75
- );
71
+ const clientSecret = (input.clientSecret ?? env.INCORTA_CLIENT_SECRET)?.trim() || void 0;
76
72
  const secret = required(
77
73
  input.secret ?? env.INCORTA_AUTH_SECRET,
78
74
  "secret",
@@ -128,7 +124,7 @@ function resolveConfig(input = {}) {
128
124
  }
129
125
 
130
126
  // src/core/handler.ts
131
- var import_node_crypto2 = require("crypto");
127
+ var import_node_crypto3 = require("crypto");
132
128
 
133
129
  // src/core/app-access.ts
134
130
  var VIEW_BIT = 1;
@@ -314,12 +310,22 @@ async function resolve(config) {
314
310
  };
315
311
  }
316
312
 
317
- // src/core/session.ts
313
+ // src/core/pkce.ts
318
314
  var import_node_crypto = require("crypto");
315
+ function generateCodeVerifier() {
316
+ return (0, import_node_crypto.randomBytes)(32).toString("base64url");
317
+ }
318
+ function codeChallengeFor(codeVerifier) {
319
+ return (0, import_node_crypto.createHash)("sha256").update(codeVerifier, "ascii").digest("base64url");
320
+ }
321
+ var CODE_CHALLENGE_METHOD = "S256";
322
+
323
+ // src/core/session.ts
324
+ var import_node_crypto2 = require("crypto");
319
325
  var import_jose2 = require("jose");
320
326
  function sealKey(config) {
321
327
  return new Uint8Array(
322
- (0, import_node_crypto.createHash)("sha256").update(`incorta-auth:${config.secret}`).digest()
328
+ (0, import_node_crypto2.createHash)("sha256").update(`incorta-auth:${config.secret}`).digest()
323
329
  );
324
330
  }
325
331
  async function seal(config, purpose, data, maxAgeSeconds) {
@@ -358,6 +364,7 @@ async function unsealLoginState(config, cookieValue) {
358
364
  if (!result) return null;
359
365
  const { data } = result;
360
366
  if (!data || typeof data.state !== "string" || typeof data.nonce !== "string") return null;
367
+ if (data.codeVerifier !== void 0 && typeof data.codeVerifier !== "string") return null;
361
368
  return data;
362
369
  }
363
370
 
@@ -371,12 +378,19 @@ var IncortaAuthError = class extends Error {
371
378
  }
372
379
  code;
373
380
  };
374
- function basicAuth(config) {
375
- const raw = `${config.clientId}:${config.clientSecret}`;
381
+ function basicAuth(clientId, clientSecret) {
382
+ const raw = `${clientId}:${clientSecret}`;
376
383
  return `Basic ${Buffer.from(raw, "utf8").toString("base64")}`;
377
384
  }
385
+ function clientAuthentication(config, params) {
386
+ if (config.clientSecret) {
387
+ return { headers: { authorization: basicAuth(config.clientId, config.clientSecret) }, params };
388
+ }
389
+ return { headers: {}, params: { client_id: config.clientId, ...params } };
390
+ }
378
391
  async function callTokenEndpoint(config, params) {
379
392
  const { tokenEndpoint } = await getEndpoints(config);
393
+ const auth = clientAuthentication(config, params);
380
394
  let response;
381
395
  try {
382
396
  response = await config.fetch(tokenEndpoint, {
@@ -384,9 +398,9 @@ async function callTokenEndpoint(config, params) {
384
398
  headers: {
385
399
  "content-type": "application/x-www-form-urlencoded",
386
400
  accept: "application/json",
387
- authorization: basicAuth(config)
401
+ ...auth.headers
388
402
  },
389
- body: new URLSearchParams(params).toString()
403
+ body: new URLSearchParams(auth.params).toString()
390
404
  });
391
405
  } catch (error) {
392
406
  throw new IncortaAuthError(
@@ -417,11 +431,12 @@ async function callTokenEndpoint(config, params) {
417
431
  refreshToken: body.refresh_token
418
432
  };
419
433
  }
420
- function exchangeCode(config, code, redirectUri) {
434
+ function exchangeCode(config, code, redirectUri, codeVerifier) {
421
435
  return callTokenEndpoint(config, {
422
436
  grant_type: "authorization_code",
423
437
  code,
424
- redirect_uri: redirectUri
438
+ redirect_uri: redirectUri,
439
+ ...codeVerifier ? { code_verifier: codeVerifier } : {}
425
440
  });
426
441
  }
427
442
  function refreshTokens(config, refreshToken) {
@@ -519,7 +534,7 @@ function withErrorParam(target, code) {
519
534
  return `${target}${separator}incorta_auth_error=${encodeURIComponent(code)}`;
520
535
  }
521
536
  function randomToken() {
522
- return (0, import_node_crypto2.randomBytes)(16).toString("base64url");
537
+ return (0, import_node_crypto3.randomBytes)(16).toString("base64url");
523
538
  }
524
539
  function escapeHtml(value) {
525
540
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
@@ -551,9 +566,10 @@ function createHandler(config) {
551
566
  if (existing) return redirect(returnTo);
552
567
  const state = randomToken();
553
568
  const nonce = randomToken();
569
+ const codeVerifier = generateCodeVerifier();
554
570
  const stateCookie = serializeCookie(
555
571
  config.stateCookieName,
556
- await sealLoginState(config, { state, nonce, returnTo }),
572
+ await sealLoginState(config, { state, nonce, returnTo, codeVerifier }),
557
573
  { ...sessionCookieOptions(secure), maxAge: 600 }
558
574
  );
559
575
  const { authorizationEndpoint } = await getEndpoints(config);
@@ -564,6 +580,8 @@ function createHandler(config) {
564
580
  authorizeUrl.searchParams.set("scope", config.scope);
565
581
  authorizeUrl.searchParams.set("state", state);
566
582
  authorizeUrl.searchParams.set("nonce", nonce);
583
+ authorizeUrl.searchParams.set("code_challenge", codeChallengeFor(codeVerifier));
584
+ authorizeUrl.searchParams.set("code_challenge_method", CODE_CHALLENGE_METHOD);
567
585
  if (config.embeddedSignIn === "new-tab") {
568
586
  return newTabSignInPage(authorizeUrl.toString(), `${origin}/`, [stateCookie]);
569
587
  }
@@ -585,7 +603,12 @@ function createHandler(config) {
585
603
  if (!loginState) return fail("state_missing");
586
604
  if (state !== loginState.state) return fail("state_mismatch");
587
605
  try {
588
- const tokens = await exchangeCode(config, code, `${origin}${config.basePath}/callback`);
606
+ const tokens = await exchangeCode(
607
+ config,
608
+ code,
609
+ `${origin}${config.basePath}/callback`,
610
+ loginState.codeVerifier
611
+ );
589
612
  const user = tokens.idToken ? await verifyIdToken(config, tokens.idToken, loginState.nonce) : await userFromAccessToken(config, tokens.accessToken);
590
613
  const session = {
591
614
  user,
@@ -1019,14 +1042,13 @@ async function registerClient(options) {
1019
1042
  })
1020
1043
  });
1021
1044
  const body = await readJson(response, "Client registration");
1022
- if (!body.client_id || !body.client_secret) {
1023
- throw new Error(
1024
- `[incorta-auth] Client registration failed: response is missing client_id/client_secret.`
1025
- );
1045
+ if (!body.client_id) {
1046
+ throw new Error(`[incorta-auth] Client registration failed: response is missing client_id.`);
1026
1047
  }
1027
1048
  return {
1028
1049
  clientId: body.client_id,
1029
- clientSecret: body.client_secret,
1050
+ clientSecret: body.client_secret || void 0,
1051
+ tokenEndpointAuthMethod: body.token_endpoint_auth_method,
1030
1052
  registrationAccessToken: body.registration_access_token,
1031
1053
  registrationClientUri: body.registration_client_uri
1032
1054
  };