@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 +14 -4
- package/dist/{app-access-BSsKtvbi.d.cts → app-access-B5lrn34s.d.cts} +9 -2
- package/dist/{app-access-BSsKtvbi.d.ts → app-access-B5lrn34s.d.ts} +9 -2
- package/dist/cli.js +11 -6
- package/dist/express.d.cts +1 -1
- package/dist/express.d.ts +1 -1
- package/dist/index.cjs +44 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -6
- package/dist/index.d.ts +15 -6
- package/dist/index.js +44 -22
- package/dist/index.js.map +1 -1
- package/dist/node.d.cts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/react/index.d.cts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/package.json +2 -2
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)
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
`
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
package/dist/express.d.cts
CHANGED
|
@@ -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-
|
|
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-
|
|
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 =
|
|
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
|
|
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/
|
|
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,
|
|
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(
|
|
375
|
-
const raw = `${
|
|
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
|
-
|
|
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,
|
|
537
|
+
return (0, import_node_crypto3.randomBytes)(16).toString("base64url");
|
|
523
538
|
}
|
|
524
539
|
function escapeHtml(value) {
|
|
525
540
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
@@ -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(
|
|
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
|
|
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
|
};
|