@kanzo-tech/auth 0.29.1 → 0.30.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 +36 -23
- package/dist/bff-auth.d.ts +1 -2
- package/dist/bff-auth.d.ts.map +1 -1
- package/dist/bff-auth.js +82 -77
- package/dist/bff-auth.js.map +1 -1
- package/dist/can.d.ts +13 -9
- package/dist/can.d.ts.map +1 -1
- package/dist/can.js +6 -6
- package/dist/can.js.map +1 -1
- package/dist/gate.d.ts +4 -4
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +8 -9
- package/dist/gate.js.map +1 -1
- package/dist/index.d.ts +11 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -23
- package/dist/index.js.map +1 -1
- package/dist/issuer.d.ts +16 -0
- package/dist/issuer.d.ts.map +1 -1
- package/dist/issuer.js +43 -25
- package/dist/issuer.js.map +1 -1
- package/dist/next-auth.d.ts +66 -0
- package/dist/next-auth.d.ts.map +1 -0
- package/dist/next-auth.js +43 -0
- package/dist/next-auth.js.map +1 -0
- package/dist/next-bound.d.ts +82 -0
- package/dist/next-bound.d.ts.map +1 -0
- package/dist/next-bound.js +27 -0
- package/dist/next-bound.js.map +1 -0
- package/dist/next-gate.d.ts +4 -0
- package/dist/next-gate.d.ts.map +1 -0
- package/dist/next-gate.js +74 -0
- package/dist/next-gate.js.map +1 -0
- package/dist/next-proxy.d.ts +15 -24
- package/dist/next-proxy.d.ts.map +1 -1
- package/dist/next-proxy.js +38 -33
- package/dist/next-proxy.js.map +1 -1
- package/dist/next-routes.d.ts +3 -21
- package/dist/next-routes.d.ts.map +1 -1
- package/dist/next-routes.js +93 -65
- package/dist/next-routes.js.map +1 -1
- package/dist/next.d.ts +16 -41
- package/dist/next.d.ts.map +1 -1
- package/dist/next.js +2 -10
- package/dist/next.js.map +1 -1
- package/dist/server.d.ts +60 -38
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +183 -159
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +102 -20
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +56 -21
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +40 -23
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/use-session.d.ts +6 -1
- package/dist/use-session.d.ts.map +1 -1
- package/dist/use-session.js +9 -7
- package/dist/use-session.js.map +1 -1
- package/package.json +10 -29
- package/dist/auth-fetch.d.ts +0 -44
- package/dist/auth-fetch.d.ts.map +0 -1
- package/dist/auth-fetch.js +0 -21
- package/dist/auth-fetch.js.map +0 -1
- package/dist/browser.d.ts +0 -61
- package/dist/browser.d.ts.map +0 -1
- package/dist/browser.js +0 -130
- package/dist/browser.js.map +0 -1
- package/dist/host.d.ts +0 -22
- package/dist/host.d.ts.map +0 -1
- package/dist/host.js +0 -10
- package/dist/host.js.map +0 -1
- package/dist/next-middleware.d.ts +0 -23
- package/dist/next-middleware.d.ts.map +0 -1
- package/dist/next-middleware.js +0 -23
- package/dist/next-middleware.js.map +0 -1
- package/dist/next-session.d.ts +0 -44
- package/dist/next-session.d.ts.map +0 -1
- package/dist/next-session.js +0 -11
- package/dist/next-session.js.map +0 -1
- package/dist/next-token.d.ts +0 -56
- package/dist/next-token.d.ts.map +0 -1
- package/dist/next-token.js +0 -9
- package/dist/next-token.js.map +0 -1
- package/dist/use-organization.d.ts +0 -22
- package/dist/use-organization.d.ts.map +0 -1
- package/dist/use-organization.js +0 -20
- package/dist/use-organization.js.map +0 -1
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,OAAO,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AACzD,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AACtE,OAAO,EAAE,GAAG,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EACL,SAAS,EACT,KAAK,IAAI,EACT,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,OAAO,EACZ,KAAK,aAAa,GACnB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,29 +1,23 @@
|
|
|
1
|
-
import { accountUrl as
|
|
2
|
-
import {
|
|
3
|
-
import { AuthProvider as a } from "./auth-provider.js";
|
|
1
|
+
import { accountUrl as e } from "./account.js";
|
|
2
|
+
import { AuthProvider as f } from "./auth-provider.js";
|
|
4
3
|
import { bffAuth as p, readSession as x } from "./bff-auth.js";
|
|
5
|
-
import { can as
|
|
6
|
-
import { claims as
|
|
7
|
-
import { Gate as
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { useOrganization as v } from "./use-organization.js";
|
|
12
|
-
import { useSession as G } from "./use-session.js";
|
|
4
|
+
import { can as a, organizationOf as n } from "./can.js";
|
|
5
|
+
import { claims as u } from "./claims.js";
|
|
6
|
+
import { Gate as h } from "./gate.js";
|
|
7
|
+
import { singleFlight as g } from "./single-flight.js";
|
|
8
|
+
import { AuthError as d } from "./types.js";
|
|
9
|
+
import { useSession as b } from "./use-session.js";
|
|
13
10
|
export {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
f as authFetch,
|
|
11
|
+
d as AuthError,
|
|
12
|
+
f as AuthProvider,
|
|
13
|
+
h as Gate,
|
|
14
|
+
e as accountUrl,
|
|
19
15
|
p as bffAuth,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
u as organizationOf,
|
|
16
|
+
a as can,
|
|
17
|
+
u as claims,
|
|
18
|
+
n as organizationOf,
|
|
24
19
|
x as readSession,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
G as useSession
|
|
20
|
+
g as singleFlight,
|
|
21
|
+
b as useSession
|
|
28
22
|
};
|
|
29
23
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;"}
|
package/dist/issuer.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Configuration, CryptoKey, CustomFetch, PrivateKey } from 'openid-client';
|
|
2
|
+
import { JWTVerifyGetKey } from 'jose';
|
|
2
3
|
/**
|
|
3
4
|
* Discovery and client configuration for the confidential client behind `./server`.
|
|
4
5
|
*
|
|
@@ -77,7 +78,22 @@ export interface Issuer {
|
|
|
77
78
|
* still stale at the moment something is.
|
|
78
79
|
*/
|
|
79
80
|
rediscover(): Promise<Configuration>;
|
|
81
|
+
/**
|
|
82
|
+
* The realm's signing keys, for verifying a token this client did not fetch itself — a
|
|
83
|
+
* back-channel logout token, which Keycloak posts to us rather than handing over a channel we
|
|
84
|
+
* opened.
|
|
85
|
+
*
|
|
86
|
+
* `jose`'s remote key set over the discovered `jwks_uri`, reached through the same transport as
|
|
87
|
+
* discovery, so an `internalOrigin` applies to it too. It re-fetches on a key id it has not seen,
|
|
88
|
+
* at most once per cooldown, which is the same reactive answer to rotation as {@link rediscover}.
|
|
89
|
+
*/
|
|
90
|
+
keys(): Promise<JWTVerifyGetKey>;
|
|
80
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* `path` without its trailing slashes, in one pass. A regex like `/\/+$/` backtracks on a long
|
|
94
|
+
* run of slashes, and these strings come from configuration a library does not control.
|
|
95
|
+
*/
|
|
96
|
+
export declare function withoutTrailingSlashes(path: string): string;
|
|
81
97
|
/**
|
|
82
98
|
* A `fetch` that replaces one origin with another before sending.
|
|
83
99
|
*
|
package/dist/issuer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"issuer.d.ts","sourceRoot":"","sources":["../src/issuer.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,WAAW,EAEhB,KAAK,UAAU,EAChB,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"issuer.d.ts","sourceRoot":"","sources":["../src/issuer.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,WAAW,EAEhB,KAAK,UAAU,EAChB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAgD,KAAK,eAAe,EAAE,MAAM,MAAM,CAAC;AAI1F;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,YAAY;IAC3B,oGAAoG;IACpG,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yFAAyF;IACzF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,GAAG,UAAU,CAAC;IAC7C;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,iGAAiG;IACjG,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IACzC;;;;OAIG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IACrC;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CACrC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,MAAM;IACrB,qGAAqG;IACrG,aAAa,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IACxC;;;;;;OAMG;IACH,UAAU,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IACrC;;;;;;;;OAQG;IACH,IAAI,IAAI,OAAO,CAAC,eAAe,CAAC,CAAC;CAClC;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI3D;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,IAAI,GAAE,OAAO,UAAU,CAAC,KAAwB,GAC/C,WAAW,CAQb;AAED,wBAAgB,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAuEnD"}
|
package/dist/issuer.js
CHANGED
|
@@ -1,42 +1,60 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { allowInsecureRequests as y, enableNonRepudiationChecks as R, PrivateKeyJwt as S, ClientSecretPost as f, discovery as I, customFetch as L } from "openid-client";
|
|
2
|
+
import { createRemoteJWKSet as T, customFetch as b } from "jose";
|
|
3
|
+
import { singleFlight as g } from "./single-flight.js";
|
|
4
|
+
import { DEADLINE as m } from "./deadline.js";
|
|
5
|
+
function F(e) {
|
|
6
|
+
let t = e.length;
|
|
7
|
+
for (; t > 0 && e[t - 1] === "/"; ) t--;
|
|
8
|
+
return e.slice(0, t);
|
|
9
|
+
}
|
|
10
|
+
function K(e, t, a = globalThis.fetch) {
|
|
11
|
+
const n = new URL(e).origin, o = F(t);
|
|
12
|
+
return (s, u) => a(
|
|
13
|
+
s.startsWith(n) ? `${o}${s.slice(n.length)}` : s,
|
|
14
|
+
u
|
|
9
15
|
);
|
|
10
16
|
}
|
|
11
|
-
function
|
|
12
|
-
const
|
|
13
|
-
[
|
|
14
|
-
timeout:
|
|
15
|
-
},
|
|
16
|
-
...e.allowInsecureHttp === !0 ? [
|
|
17
|
-
...e.verifySignatures ?? !
|
|
17
|
+
function D(e) {
|
|
18
|
+
const t = new URL(e.issuer), a = e.fetch ?? ((r, h) => globalThis.fetch(r, h)), n = e.internalOrigin ?? t.origin, o = K(e.issuer, n, a), s = {
|
|
19
|
+
[L]: o,
|
|
20
|
+
timeout: m / 1e3
|
|
21
|
+
}, u = new URL(n).protocol === "https:", d = [
|
|
22
|
+
...e.allowInsecureHttp === !0 ? [y] : [],
|
|
23
|
+
...e.verifySignatures ?? !u ? [R] : []
|
|
18
24
|
];
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
let
|
|
22
|
-
const l =
|
|
23
|
-
const
|
|
24
|
-
return
|
|
25
|
+
d.length > 0 && (s.execute = d);
|
|
26
|
+
const w = e.privateKey !== void 0 ? S(e.privateKey) : f(e.clientSecret), v = e.clientSecret !== void 0 ? { client_secret: e.clientSecret } : {};
|
|
27
|
+
let c, i;
|
|
28
|
+
const l = g(async () => {
|
|
29
|
+
const r = await I(t, e.clientId, v, w, s);
|
|
30
|
+
return c = r, r;
|
|
25
31
|
});
|
|
26
32
|
return {
|
|
27
33
|
async configuration() {
|
|
28
|
-
return
|
|
34
|
+
return c ?? l();
|
|
29
35
|
},
|
|
30
36
|
// A `rediscover()` that lands while a discovery is already running joins that one rather than
|
|
31
37
|
// starting a newer one. It is the narrow price of single-flight, and it is bounded: the call
|
|
32
38
|
// it joins is at most one request old, and a second failure rediscovers again.
|
|
33
39
|
async rediscover() {
|
|
34
|
-
return
|
|
40
|
+
return c = void 0, l();
|
|
41
|
+
},
|
|
42
|
+
async keys() {
|
|
43
|
+
const r = (c ?? await l()).serverMetadata().jwks_uri;
|
|
44
|
+
if (r === void 0) throw new TypeError("the IdP's discovery document names no `jwks_uri`");
|
|
45
|
+
return (i == null ? void 0 : i.uri) !== r && (i = {
|
|
46
|
+
uri: r,
|
|
47
|
+
set: T(new URL(r), {
|
|
48
|
+
[b]: (h, p) => o(h, p),
|
|
49
|
+
timeoutDuration: m
|
|
50
|
+
})
|
|
51
|
+
}), i.set;
|
|
35
52
|
}
|
|
36
53
|
};
|
|
37
54
|
}
|
|
38
55
|
export {
|
|
39
|
-
|
|
40
|
-
|
|
56
|
+
D as issuer,
|
|
57
|
+
K as rewriteOrigin,
|
|
58
|
+
F as withoutTrailingSlashes
|
|
41
59
|
};
|
|
42
60
|
//# sourceMappingURL=issuer.js.map
|
package/dist/issuer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"issuer.js","sources":["../src/issuer.ts"],"sourcesContent":["import {\n ClientSecretPost,\n PrivateKeyJwt,\n allowInsecureRequests,\n customFetch,\n enableNonRepudiationChecks,\n discovery,\n type ClientMetadata,\n type Configuration,\n type CryptoKey,\n type CustomFetch,\n type DiscoveryRequestOptions,\n type PrivateKey,\n} from \"openid-client\";\nimport { singleFlight } from \"./single-flight\";\nimport { DEADLINE } from \"./deadline\";\n\n/**\n * Discovery and client configuration for the confidential client behind `./server`.\n *\n * Two things live here that a plain `discovery()` call does not give you, and both come from\n * running this against Keycloak in anger:\n *\n * 1. **The issuer's origin can be rewritten for server→Keycloak traffic.** The browser must be\n * sent to the public issuer, and the server usually cannot reach it — in a cluster it reaches\n * `http://keycloak:8080`. Rewriting the *transport* rather than the issuer keeps the two\n * truthful at once: discovery still validates the `issuer` in the document against the public\n * URL, because that is what Keycloak puts there.\n * 2. **Discovery is allowed to fail.** Keycloak is frequently not up when the application is, and\n * a library that hides a retry loop inside itself takes that decision away from the caller. A\n * failed discovery is simply not cached, so the *next* call tries again — the retry is the\n * caller's `await`, on the caller's schedule.\n */\n\nexport interface IssuerConfig {\n /** The **public** issuer URL, exactly as Keycloak reports it: `https://id.example/realms/kanzo`. */\n readonly issuer: string;\n readonly clientId: string;\n /** Client secret authentication. Used when {@link IssuerConfig.privateKey} is absent. */\n readonly clientSecret?: string;\n /**\n * `private_key_jwt`, which is the stronger of the two: the secret never travels. Preferred\n * wherever the deployment can hold a key, and it wins over `clientSecret` when both are given.\n */\n readonly privateKey?: CryptoKey | PrivateKey;\n /**\n * Where *this process* reaches Keycloak, when that is not where the browser reaches it —\n * `http://keycloak:8080`. Only the origin is replaced; the path is the issuer's own.\n */\n readonly internalOrigin?: string;\n /** Injectable for tests, and the seam the origin rewrite is built on. Defaults to the global. */\n readonly fetch?: typeof globalThis.fetch;\n /**\n * Allow a plain-HTTP issuer. A compose file on a laptop serves `http://localhost:8080`, and\n * without this nothing local can be configured at all. It is not needed for an HTTP\n * {@link IssuerConfig.internalOrigin}: the rewrite happens below the protocol check.\n */\n readonly allowInsecureHttp?: boolean;\n /**\n * Verify the ID token's signature, and not only its claims.\n *\n * **Defaults to whether the token endpoint is actually reached over TLS**, rather than to a flat\n * `false`. The specification lets a code grant skip this — an ID token arriving over a TLS\n * connection to the token endpoint, from a request authenticated as this client, is vouched for\n * by the channel (OpenID Connect Core §3.1.3.7 step 6) — and `openid-client` leaves it off for\n * that reason, which is why a sign-in fetches no JWKS at all.\n *\n * But the exemption is a claim *about the channel*, and an {@link IssuerConfig.internalOrigin} of\n * `http://keycloak:8080` withdraws it: the hop TLS was supposed to protect is plaintext inside\n * the cluster, so nothing is vouching for anything. Inheriting the reference's default there\n * would be inheriting its conclusion without its premise. So the default is derived from the\n * effective origin's protocol, and it can still be set explicitly either way.\n *\n * On, it costs one JWKS fetch, cached — and it is what makes {@link Issuer.rediscover} reachable\n * on a key rotation.\n */\n readonly verifySignatures?: boolean;\n}\n\n/**\n * A configuration that discovers on demand.\n *\n * Holding the handle rather than the {@link Configuration} is what makes rotation expressible:\n * `rediscover()` throws the current one away, and with it the JWKS that openid-client cached\n * inside it.\n */\nexport interface Issuer {\n /** The configuration, discovering once and reusing it. Rejects — and caches nothing — on failure. */\n configuration(): Promise<Configuration>;\n /**\n * Discard what was discovered and fetch it again.\n *\n * This is the answer to signing-key rotation, and it is deliberately *reactive*: the trigger is\n * a verification that failed, never a timer. A timer refreshes when nothing is wrong and is\n * still stale at the moment something is.\n */\n rediscover(): Promise<Configuration>;\n}\n\n/**\n * A `fetch` that replaces one origin with another before sending.\n *\n * `from` may be any URL — its origin is what is taken — so the issuer URL itself can be passed\n * without the caller splitting it first. When the two origins are equal this is a pass-through,\n * which is why there is no second code path for \"no rewrite configured\".\n */\nexport function rewriteOrigin(\n from: string,\n to: string,\n base: typeof globalThis.fetch = globalThis.fetch,\n): CustomFetch {\n const source = new URL(from).origin;\n const target = to.replace(/\\/+$/, \"\");\n return (url, options) =>\n base(\n url.startsWith(source) ? `${target}${url.slice(source.length)}` : url,\n options as unknown as RequestInit,\n );\n}\n\nexport function issuer(config: IssuerConfig): Issuer {\n const server = new URL(config.issuer);\n const base: typeof globalThis.fetch =\n config.fetch ?? ((input, init) => globalThis.fetch(input, init));\n\n const reachedAt = config.internalOrigin ?? server.origin;\n\n const options: DiscoveryRequestOptions = {\n [customFetch]: rewriteOrigin(config.issuer, reachedAt, base),\n timeout: DEADLINE / 1000,\n };\n\n // The channel is what the specification's exemption rests on, so the default asks whether there\n // is one rather than assuming it. Explicit beats derived in both directions.\n const overTls = new URL(reachedAt).protocol === \"https:\";\n const execute = [\n ...(config.allowInsecureHttp === true ? [allowInsecureRequests] : []),\n ...((config.verifySignatures ?? !overTls) ? [enableNonRepudiationChecks] : []),\n ];\n if (execute.length > 0) options.execute = execute;\n\n // `private_key_jwt` over a shared secret wherever the deployment can hold a key. `None()` is\n // absent on purpose: this door is the confidential client, and a public one belongs behind\n // `./browser` where PKCE alone is the protection.\n const clientAuth =\n config.privateKey !== undefined\n ? PrivateKeyJwt(config.privateKey)\n : ClientSecretPost(config.clientSecret);\n\n const metadata: Partial<ClientMetadata> =\n config.clientSecret !== undefined ? { client_secret: config.clientSecret } : {};\n\n let current: Configuration | undefined;\n\n // Single-flight for the reason it exists everywhere in this package: six requests arriving\n // during a cold start would otherwise each fetch the discovery document. Here the slot is\n // cleared on failure, which *is* the caller-driven retry — the next `await` starts a new attempt.\n const fetchOnce = singleFlight(async () => {\n const discovered = await discovery(server, config.clientId, metadata, clientAuth, options);\n current = discovered;\n return discovered;\n });\n\n return {\n async configuration() {\n return current ?? fetchOnce();\n },\n // A `rediscover()` that lands while a discovery is already running joins that one rather than\n // starting a newer one. It is the narrow price of single-flight, and it is bounded: the call\n // it joins is at most one request old, and a second failure rediscovers again.\n async rediscover() {\n current = undefined;\n return fetchOnce();\n },\n };\n}\n"],"names":["rewriteOrigin","from","to","base","source","target","url","options","issuer","config","server","input","init","reachedAt","customFetch","DEADLINE","overTls","execute","allowInsecureRequests","enableNonRepudiationChecks","clientAuth","PrivateKeyJwt","ClientSecretPost","metadata","current","fetchOnce","singleFlight","discovered","discovery"],"mappings":";;;AA0GO,SAASA,EACdC,GACAC,GACAC,IAAgC,WAAW,OAC9B;AACb,QAAMC,IAAS,IAAI,IAAIH,CAAI,EAAE,QACvBI,IAASH,EAAG,QAAQ,QAAQ,EAAE;AACpC,SAAO,CAACI,GAAKC,MACXJ;AAAA,IACEG,EAAI,WAAWF,CAAM,IAAI,GAAGC,CAAM,GAAGC,EAAI,MAAMF,EAAO,MAAM,CAAC,KAAKE;AAAA,IAClEC;AAAA,EAAA;AAEN;AAEO,SAASC,EAAOC,GAA8B;AACnD,QAAMC,IAAS,IAAI,IAAID,EAAO,MAAM,GAC9BN,IACJM,EAAO,UAAU,CAACE,GAAOC,MAAS,WAAW,MAAMD,GAAOC,CAAI,IAE1DC,IAAYJ,EAAO,kBAAkBC,EAAO,QAE5CH,IAAmC;AAAA,IACvC,CAACO,CAAW,GAAGd,EAAcS,EAAO,QAAQI,GAAWV,CAAI;AAAA,IAC3D,SAASY,IAAW;AAAA,EAAA,GAKhBC,IAAU,IAAI,IAAIH,CAAS,EAAE,aAAa,UAC1CI,IAAU;AAAA,IACd,GAAIR,EAAO,sBAAsB,KAAO,CAACS,CAAqB,IAAI,CAAA;AAAA,IAClE,GAAKT,EAAO,oBAAoB,CAACO,IAAW,CAACG,CAA0B,IAAI,CAAA;AAAA,EAAC;AAE9E,EAAIF,EAAQ,SAAS,MAAGV,EAAQ,UAAUU;AAK1C,QAAMG,IACJX,EAAO,eAAe,SAClBY,EAAcZ,EAAO,UAAU,IAC/Ba,EAAiBb,EAAO,YAAY,GAEpCc,IACJd,EAAO,iBAAiB,SAAY,EAAE,eAAeA,EAAO,aAAA,IAAiB,CAAA;AAE/E,MAAIe;AAKJ,QAAMC,IAAYC,EAAa,YAAY;AACzC,UAAMC,IAAa,MAAMC,EAAUlB,GAAQD,EAAO,UAAUc,GAAUH,GAAYb,CAAO;AACzF,WAAAiB,IAAUG,GACHA;AAAA,EACT,CAAC;AAED,SAAO;AAAA,IACL,MAAM,gBAAgB;AACpB,aAAOH,KAAWC,EAAA;AAAA,IACpB;AAAA;AAAA;AAAA;AAAA,IAIA,MAAM,aAAa;AACjB,aAAAD,IAAU,QACHC,EAAA;AAAA,IACT;AAAA,EAAA;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"issuer.js","sources":["../src/issuer.ts"],"sourcesContent":["import {\n ClientSecretPost,\n PrivateKeyJwt,\n allowInsecureRequests,\n customFetch,\n enableNonRepudiationChecks,\n discovery,\n type ClientMetadata,\n type Configuration,\n type CryptoKey,\n type CustomFetch,\n type DiscoveryRequestOptions,\n type PrivateKey,\n} from \"openid-client\";\nimport { createRemoteJWKSet, customFetch as jwksFetch, type JWTVerifyGetKey } from \"jose\";\nimport { singleFlight } from \"./single-flight\";\nimport { DEADLINE } from \"./deadline\";\n\n/**\n * Discovery and client configuration for the confidential client behind `./server`.\n *\n * Two things live here that a plain `discovery()` call does not give you, and both come from\n * running this against Keycloak in anger:\n *\n * 1. **The issuer's origin can be rewritten for server→Keycloak traffic.** The browser must be\n * sent to the public issuer, and the server usually cannot reach it — in a cluster it reaches\n * `http://keycloak:8080`. Rewriting the *transport* rather than the issuer keeps the two\n * truthful at once: discovery still validates the `issuer` in the document against the public\n * URL, because that is what Keycloak puts there.\n * 2. **Discovery is allowed to fail.** Keycloak is frequently not up when the application is, and\n * a library that hides a retry loop inside itself takes that decision away from the caller. A\n * failed discovery is simply not cached, so the *next* call tries again — the retry is the\n * caller's `await`, on the caller's schedule.\n */\n\nexport interface IssuerConfig {\n /** The **public** issuer URL, exactly as Keycloak reports it: `https://id.example/realms/kanzo`. */\n readonly issuer: string;\n readonly clientId: string;\n /** Client secret authentication. Used when {@link IssuerConfig.privateKey} is absent. */\n readonly clientSecret?: string;\n /**\n * `private_key_jwt`, which is the stronger of the two: the secret never travels. Preferred\n * wherever the deployment can hold a key, and it wins over `clientSecret` when both are given.\n */\n readonly privateKey?: CryptoKey | PrivateKey;\n /**\n * Where *this process* reaches Keycloak, when that is not where the browser reaches it —\n * `http://keycloak:8080`. Only the origin is replaced; the path is the issuer's own.\n */\n readonly internalOrigin?: string;\n /** Injectable for tests, and the seam the origin rewrite is built on. Defaults to the global. */\n readonly fetch?: typeof globalThis.fetch;\n /**\n * Allow a plain-HTTP issuer. A compose file on a laptop serves `http://localhost:8080`, and\n * without this nothing local can be configured at all. It is not needed for an HTTP\n * {@link IssuerConfig.internalOrigin}: the rewrite happens below the protocol check.\n */\n readonly allowInsecureHttp?: boolean;\n /**\n * Verify the ID token's signature, and not only its claims.\n *\n * **Defaults to whether the token endpoint is actually reached over TLS**, rather than to a flat\n * `false`. The specification lets a code grant skip this — an ID token arriving over a TLS\n * connection to the token endpoint, from a request authenticated as this client, is vouched for\n * by the channel (OpenID Connect Core §3.1.3.7 step 6) — and `openid-client` leaves it off for\n * that reason, which is why a sign-in fetches no JWKS at all.\n *\n * But the exemption is a claim *about the channel*, and an {@link IssuerConfig.internalOrigin} of\n * `http://keycloak:8080` withdraws it: the hop TLS was supposed to protect is plaintext inside\n * the cluster, so nothing is vouching for anything. Inheriting the reference's default there\n * would be inheriting its conclusion without its premise. So the default is derived from the\n * effective origin's protocol, and it can still be set explicitly either way.\n *\n * On, it costs one JWKS fetch, cached — and it is what makes {@link Issuer.rediscover} reachable\n * on a key rotation.\n */\n readonly verifySignatures?: boolean;\n}\n\n/**\n * A configuration that discovers on demand.\n *\n * Holding the handle rather than the {@link Configuration} is what makes rotation expressible:\n * `rediscover()` throws the current one away, and with it the JWKS that openid-client cached\n * inside it.\n */\nexport interface Issuer {\n /** The configuration, discovering once and reusing it. Rejects — and caches nothing — on failure. */\n configuration(): Promise<Configuration>;\n /**\n * Discard what was discovered and fetch it again.\n *\n * This is the answer to signing-key rotation, and it is deliberately *reactive*: the trigger is\n * a verification that failed, never a timer. A timer refreshes when nothing is wrong and is\n * still stale at the moment something is.\n */\n rediscover(): Promise<Configuration>;\n /**\n * The realm's signing keys, for verifying a token this client did not fetch itself — a\n * back-channel logout token, which Keycloak posts to us rather than handing over a channel we\n * opened.\n *\n * `jose`'s remote key set over the discovered `jwks_uri`, reached through the same transport as\n * discovery, so an `internalOrigin` applies to it too. It re-fetches on a key id it has not seen,\n * at most once per cooldown, which is the same reactive answer to rotation as {@link rediscover}.\n */\n keys(): Promise<JWTVerifyGetKey>;\n}\n\n/**\n * `path` without its trailing slashes, in one pass. A regex like `/\\/+$/` backtracks on a long\n * run of slashes, and these strings come from configuration a library does not control.\n */\nexport function withoutTrailingSlashes(path: string): string {\n let end = path.length;\n while (end > 0 && path[end - 1] === \"/\") end--;\n return path.slice(0, end);\n}\n\n/**\n * A `fetch` that replaces one origin with another before sending.\n *\n * `from` may be any URL — its origin is what is taken — so the issuer URL itself can be passed\n * without the caller splitting it first. When the two origins are equal this is a pass-through,\n * which is why there is no second code path for \"no rewrite configured\".\n */\nexport function rewriteOrigin(\n from: string,\n to: string,\n base: typeof globalThis.fetch = globalThis.fetch,\n): CustomFetch {\n const source = new URL(from).origin;\n const target = withoutTrailingSlashes(to);\n return (url, options) =>\n base(\n url.startsWith(source) ? `${target}${url.slice(source.length)}` : url,\n options as unknown as RequestInit,\n );\n}\n\nexport function issuer(config: IssuerConfig): Issuer {\n const server = new URL(config.issuer);\n const base: typeof globalThis.fetch =\n config.fetch ?? ((input, init) => globalThis.fetch(input, init));\n\n const reachedAt = config.internalOrigin ?? server.origin;\n\n const transport = rewriteOrigin(config.issuer, reachedAt, base);\n const options: DiscoveryRequestOptions = {\n [customFetch]: transport,\n timeout: DEADLINE / 1000,\n };\n\n // The channel is what the specification's exemption rests on, so the default asks whether there\n // is one rather than assuming it. Explicit beats derived in both directions.\n const overTls = new URL(reachedAt).protocol === \"https:\";\n const execute = [\n ...(config.allowInsecureHttp === true ? [allowInsecureRequests] : []),\n ...((config.verifySignatures ?? !overTls) ? [enableNonRepudiationChecks] : []),\n ];\n if (execute.length > 0) options.execute = execute;\n\n // `private_key_jwt` over a shared secret wherever the deployment can hold a key. `None()` is\n // absent on purpose: this door is the confidential client, and a public one belongs behind\n // `./browser` where PKCE alone is the protection.\n const clientAuth =\n config.privateKey !== undefined\n ? PrivateKeyJwt(config.privateKey)\n : ClientSecretPost(config.clientSecret);\n\n const metadata: Partial<ClientMetadata> =\n config.clientSecret !== undefined ? { client_secret: config.clientSecret } : {};\n\n let current: Configuration | undefined;\n let keys: { readonly uri: string; readonly set: JWTVerifyGetKey } | undefined;\n\n // Single-flight for the reason it exists everywhere in this package: six requests arriving\n // during a cold start would otherwise each fetch the discovery document. Here the slot is\n // cleared on failure, which *is* the caller-driven retry — the next `await` starts a new attempt.\n const fetchOnce = singleFlight(async () => {\n const discovered = await discovery(server, config.clientId, metadata, clientAuth, options);\n current = discovered;\n return discovered;\n });\n\n return {\n async configuration() {\n return current ?? fetchOnce();\n },\n // A `rediscover()` that lands while a discovery is already running joins that one rather than\n // starting a newer one. It is the narrow price of single-flight, and it is bounded: the call\n // it joins is at most one request old, and a second failure rediscovers again.\n async rediscover() {\n current = undefined;\n return fetchOnce();\n },\n async keys() {\n const uri = (current ?? (await fetchOnce())).serverMetadata().jwks_uri;\n if (uri === undefined) throw new TypeError(\"the IdP's discovery document names no `jwks_uri`\");\n if (keys?.uri !== uri) {\n keys = {\n uri,\n set: createRemoteJWKSet(new URL(uri), {\n [jwksFetch]: (url, init) => transport(url, init as never),\n timeoutDuration: DEADLINE,\n }),\n };\n }\n return keys.set;\n },\n };\n}\n"],"names":["withoutTrailingSlashes","path","end","rewriteOrigin","from","to","base","source","target","url","options","issuer","config","server","input","init","reachedAt","transport","customFetch","DEADLINE","overTls","execute","allowInsecureRequests","enableNonRepudiationChecks","clientAuth","PrivateKeyJwt","ClientSecretPost","metadata","current","keys","fetchOnce","singleFlight","discovered","discovery","uri","createRemoteJWKSet","jwksFetch"],"mappings":";;;;AAkHO,SAASA,EAAuBC,GAAsB;AAC3D,MAAIC,IAAMD,EAAK;AACf,SAAOC,IAAM,KAAKD,EAAKC,IAAM,CAAC,MAAM,MAAK,CAAAA;AACzC,SAAOD,EAAK,MAAM,GAAGC,CAAG;AAC1B;AASO,SAASC,EACdC,GACAC,GACAC,IAAgC,WAAW,OAC9B;AACb,QAAMC,IAAS,IAAI,IAAIH,CAAI,EAAE,QACvBI,IAASR,EAAuBK,CAAE;AACxC,SAAO,CAACI,GAAKC,MACXJ;AAAA,IACEG,EAAI,WAAWF,CAAM,IAAI,GAAGC,CAAM,GAAGC,EAAI,MAAMF,EAAO,MAAM,CAAC,KAAKE;AAAA,IAClEC;AAAA,EAAA;AAEN;AAEO,SAASC,EAAOC,GAA8B;AACnD,QAAMC,IAAS,IAAI,IAAID,EAAO,MAAM,GAC9BN,IACJM,EAAO,UAAU,CAACE,GAAOC,MAAS,WAAW,MAAMD,GAAOC,CAAI,IAE1DC,IAAYJ,EAAO,kBAAkBC,EAAO,QAE5CI,IAAYd,EAAcS,EAAO,QAAQI,GAAWV,CAAI,GACxDI,IAAmC;AAAA,IACvC,CAACQ,CAAW,GAAGD;AAAA,IACf,SAASE,IAAW;AAAA,EAAA,GAKhBC,IAAU,IAAI,IAAIJ,CAAS,EAAE,aAAa,UAC1CK,IAAU;AAAA,IACd,GAAIT,EAAO,sBAAsB,KAAO,CAACU,CAAqB,IAAI,CAAA;AAAA,IAClE,GAAKV,EAAO,oBAAoB,CAACQ,IAAW,CAACG,CAA0B,IAAI,CAAA;AAAA,EAAC;AAE9E,EAAIF,EAAQ,SAAS,MAAGX,EAAQ,UAAUW;AAK1C,QAAMG,IACJZ,EAAO,eAAe,SAClBa,EAAcb,EAAO,UAAU,IAC/Bc,EAAiBd,EAAO,YAAY,GAEpCe,IACJf,EAAO,iBAAiB,SAAY,EAAE,eAAeA,EAAO,aAAA,IAAiB,CAAA;AAE/E,MAAIgB,GACAC;AAKJ,QAAMC,IAAYC,EAAa,YAAY;AACzC,UAAMC,IAAa,MAAMC,EAAUpB,GAAQD,EAAO,UAAUe,GAAUH,GAAYd,CAAO;AACzF,WAAAkB,IAAUI,GACHA;AAAA,EACT,CAAC;AAED,SAAO;AAAA,IACL,MAAM,gBAAgB;AACpB,aAAOJ,KAAWE,EAAA;AAAA,IACpB;AAAA;AAAA;AAAA;AAAA,IAIA,MAAM,aAAa;AACjB,aAAAF,IAAU,QACHE,EAAA;AAAA,IACT;AAAA,IACA,MAAM,OAAO;AACX,YAAMI,KAAON,KAAY,MAAME,EAAA,GAAc,iBAAiB;AAC9D,UAAII,MAAQ,OAAW,OAAM,IAAI,UAAU,kDAAkD;AAC7F,cAAIL,KAAA,gBAAAA,EAAM,SAAQK,MAChBL,IAAO;AAAA,QACL,KAAAK;AAAA,QACA,KAAKC,EAAmB,IAAI,IAAID,CAAG,GAAG;AAAA,UACpC,CAACE,CAAS,GAAG,CAAC3B,GAAKM,MAASE,EAAUR,GAAKM,CAAa;AAAA,UACxD,iBAAiBI;AAAA,QAAA,CAClB;AAAA,MAAA,IAGEU,EAAK;AAAA,IACd;AAAA,EAAA;AAEJ;"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { KanzoAuthConfig } from './next-bound';
|
|
3
|
+
import { ApiHandlers } from './next-proxy';
|
|
4
|
+
import { RouteHandlers } from './next-routes';
|
|
5
|
+
import { Session } from './types';
|
|
6
|
+
export type { KanzoAuthConfig } from './next-bound';
|
|
7
|
+
/**
|
|
8
|
+
* `kanzoAuth` — the whole Backend For Frontend as one object: a proxy that is the session
|
|
9
|
+
* authority, the auth routes, the resource-server forwarder and the session a server component
|
|
10
|
+
* reads.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* // lib/auth.ts
|
|
14
|
+
* export const auth = kanzoAuth(async () => ({ issuer, clientId, clientSecret, secret, store }));
|
|
15
|
+
*
|
|
16
|
+
* // proxy.ts
|
|
17
|
+
* export const proxy = (request: NextRequest) => auth.proxy(request);
|
|
18
|
+
*
|
|
19
|
+
* // app/api/auth/[...auth]/route.ts
|
|
20
|
+
* export const { GET, POST } = auth.routes;
|
|
21
|
+
*
|
|
22
|
+
* // any server component
|
|
23
|
+
* const session = await auth.session({ required: true });
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* ## One object, because one relying party
|
|
27
|
+
*
|
|
28
|
+
* Auth0's v4 SDK is one `Auth0Client` for the same reason. Every piece here talks to the same
|
|
29
|
+
* issuer with the same secret and the same store, and a renewal in the proxy must be the renewal
|
|
30
|
+
* the forwarder joins. Five factories each holding their own relying party meant five discovery
|
|
31
|
+
* caches and five configs that had to agree by hand; one instance means they cannot disagree.
|
|
32
|
+
*
|
|
33
|
+
* ## Bound on first request, not at import
|
|
34
|
+
*
|
|
35
|
+
* `next build` imports every route module with no secrets in the environment, so the config is a
|
|
36
|
+
* function — possibly async, for a secret read from a file or a vault — called the first time a
|
|
37
|
+
* request needs it. The built instance is remembered; a failure is not, so a deployment that
|
|
38
|
+
* started before its IdP or its secret was ready recovers on the next request instead of
|
|
39
|
+
* remembering the outage. `routes` and `api` are plain handler objects that exist at import, which
|
|
40
|
+
* is what a route file's `export const { GET, POST } = …` needs.
|
|
41
|
+
*/
|
|
42
|
+
export interface KanzoAuth {
|
|
43
|
+
/**
|
|
44
|
+
* Next's `proxy.ts`: the session authority for every page.
|
|
45
|
+
*
|
|
46
|
+
* It renews an access token that is about to expire, ends a session the IdP refused to renew,
|
|
47
|
+
* and sends a browser with no live session to sign in carrying the URL it asked for — or, for a
|
|
48
|
+
* request that is not a navigation, answers 401. `headers` are added to the request the page
|
|
49
|
+
* sees, for a CSP nonce.
|
|
50
|
+
*/
|
|
51
|
+
proxy(request: NextRequest, options?: {
|
|
52
|
+
readonly headers?: HeadersInit;
|
|
53
|
+
}): Promise<NextResponse>;
|
|
54
|
+
/** `signin`, `callback`, `signout`, `session`, `refresh` and `backchannel-logout`, for `[...auth]/route.ts`. */
|
|
55
|
+
readonly routes: RouteHandlers;
|
|
56
|
+
/** Every verb a route file exports, forwarding to `api.target`. 404 without `api`. */
|
|
57
|
+
readonly api: ApiHandlers;
|
|
58
|
+
/** The session this request carries, read once per request, or `null`. */
|
|
59
|
+
session(): Promise<Session | null>;
|
|
60
|
+
/** The session, or a redirect to sign in that comes back to this page. */
|
|
61
|
+
session(options: {
|
|
62
|
+
readonly required: true;
|
|
63
|
+
}): Promise<Session>;
|
|
64
|
+
}
|
|
65
|
+
export declare function kanzoAuth(config: KanzoAuthConfig | (() => KanzoAuthConfig | Promise<KanzoAuthConfig>)): KanzoAuth;
|
|
66
|
+
//# sourceMappingURL=next-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next-auth.d.ts","sourceRoot":"","sources":["../src/next-auth.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE7D,OAAO,EAA4C,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAE9F,OAAO,EAAW,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAU,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAE3D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,MAAM,WAAW,SAAS;IACxB;;;;;;;OAOG;IACH,KAAK,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACjG,gHAAgH;IAChH,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,sFAAsF;IACtF,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;IAC1B,0EAA0E;IAC1E,OAAO,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IACnC,0EAA0E;IAC1E,OAAO,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACjE;AAmBD,wBAAgB,SAAS,CACvB,MAAM,EAAE,eAAe,GAAG,CAAC,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC,GAC3E,SAAS,CAyCX"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { cookies as m, headers as d } from "next/headers";
|
|
2
|
+
import { redirect as f } from "next/navigation";
|
|
3
|
+
import { cache as h } from "react";
|
|
4
|
+
import { withTenant as w, URL_HEADER as y, bind as p } from "./next-bound.js";
|
|
5
|
+
import { gate as g } from "./next-gate.js";
|
|
6
|
+
import { forward as $ } from "./next-proxy.js";
|
|
7
|
+
import { routes as b } from "./next-routes.js";
|
|
8
|
+
import { singleFlight as R } from "./single-flight.js";
|
|
9
|
+
function U(n) {
|
|
10
|
+
const e = n.get(y);
|
|
11
|
+
if (e !== null)
|
|
12
|
+
try {
|
|
13
|
+
return new URL(e);
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
return new URL(`https://${n.get("host") ?? "localhost"}/`);
|
|
17
|
+
}
|
|
18
|
+
function j(n) {
|
|
19
|
+
let e;
|
|
20
|
+
const c = R(async () => (e = p(typeof n == "function" ? await n() : n), e)), a = async () => e ?? c(), u = h(async () => {
|
|
21
|
+
const t = await a(), [r, o] = await Promise.all([m(), d()]), s = U(o), i = await t.party.read(r.toString());
|
|
22
|
+
return i === null ? { session: null, url: s } : {
|
|
23
|
+
session: w(i, await t.tenant({ url: s, headers: o, cookies: r })),
|
|
24
|
+
url: s
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
async function l(t) {
|
|
28
|
+
const { session: r, url: o } = await u();
|
|
29
|
+
if (r !== null || (t == null ? void 0 : t.required) !== !0) return r;
|
|
30
|
+
const s = `${(await a()).basePath}/signin`;
|
|
31
|
+
f(`${s}?returnTo=${encodeURIComponent(`${o.pathname}${o.search}`)}`);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
proxy: async (t, r) => g(await a(), t, r == null ? void 0 : r.headers),
|
|
35
|
+
routes: b(a),
|
|
36
|
+
api: $(a),
|
|
37
|
+
session: l
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
j as kanzoAuth
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=next-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next-auth.js","sources":["../src/next-auth.ts"],"sourcesContent":["import { cookies, headers } from \"next/headers\";\nimport { redirect } from \"next/navigation\";\nimport type { NextRequest, NextResponse } from \"next/server\";\nimport { cache } from \"react\";\nimport { bind, URL_HEADER, withTenant, type Bound, type KanzoAuthConfig } from \"./next-bound\";\nimport { gate } from \"./next-gate\";\nimport { forward, type ApiHandlers } from \"./next-proxy\";\nimport { routes, type RouteHandlers } from \"./next-routes\";\nimport { singleFlight } from \"./single-flight\";\nimport type { Session } from \"./types\";\n\nexport type { KanzoAuthConfig } from \"./next-bound\";\n\n/**\n * `kanzoAuth` — the whole Backend For Frontend as one object: a proxy that is the session\n * authority, the auth routes, the resource-server forwarder and the session a server component\n * reads.\n *\n * ```ts\n * // lib/auth.ts\n * export const auth = kanzoAuth(async () => ({ issuer, clientId, clientSecret, secret, store }));\n *\n * // proxy.ts\n * export const proxy = (request: NextRequest) => auth.proxy(request);\n *\n * // app/api/auth/[...auth]/route.ts\n * export const { GET, POST } = auth.routes;\n *\n * // any server component\n * const session = await auth.session({ required: true });\n * ```\n *\n * ## One object, because one relying party\n *\n * Auth0's v4 SDK is one `Auth0Client` for the same reason. Every piece here talks to the same\n * issuer with the same secret and the same store, and a renewal in the proxy must be the renewal\n * the forwarder joins. Five factories each holding their own relying party meant five discovery\n * caches and five configs that had to agree by hand; one instance means they cannot disagree.\n *\n * ## Bound on first request, not at import\n *\n * `next build` imports every route module with no secrets in the environment, so the config is a\n * function — possibly async, for a secret read from a file or a vault — called the first time a\n * request needs it. The built instance is remembered; a failure is not, so a deployment that\n * started before its IdP or its secret was ready recovers on the next request instead of\n * remembering the outage. `routes` and `api` are plain handler objects that exist at import, which\n * is what a route file's `export const { GET, POST } = …` needs.\n */\n\nexport interface KanzoAuth {\n /**\n * Next's `proxy.ts`: the session authority for every page.\n *\n * It renews an access token that is about to expire, ends a session the IdP refused to renew,\n * and sends a browser with no live session to sign in carrying the URL it asked for — or, for a\n * request that is not a navigation, answers 401. `headers` are added to the request the page\n * sees, for a CSP nonce.\n */\n proxy(request: NextRequest, options?: { readonly headers?: HeadersInit }): Promise<NextResponse>;\n /** `signin`, `callback`, `signout`, `session`, `refresh` and `backchannel-logout`, for `[...auth]/route.ts`. */\n readonly routes: RouteHandlers;\n /** Every verb a route file exports, forwarding to `api.target`. 404 without `api`. */\n readonly api: ApiHandlers;\n /** The session this request carries, read once per request, or `null`. */\n session(): Promise<Session | null>;\n /** The session, or a redirect to sign in that comes back to this page. */\n session(options: { readonly required: true }): Promise<Session>;\n}\n\n/**\n * The URL a server component is rendering, from the header the proxy set — which it deletes from\n * every incoming request first, so a browser cannot choose it. A page the proxy does not run on\n * has only its host.\n */\nfunction renderedUrl(incoming: Headers): URL {\n const forwarded = incoming.get(URL_HEADER);\n if (forwarded !== null) {\n try {\n return new URL(forwarded);\n } catch {\n /* not a URL: read as absent */\n }\n }\n return new URL(`https://${incoming.get(\"host\") ?? \"localhost\"}/`);\n}\n\nexport function kanzoAuth(\n config: KanzoAuthConfig | (() => KanzoAuthConfig | Promise<KanzoAuthConfig>),\n): KanzoAuth {\n // The instance is kept and the attempt is not: concurrent first requests share one build, and a\n // build that threw leaves nothing behind, so the next request builds again — `issuer`'s shape.\n let bound: Bound | undefined;\n const build = singleFlight(async () => {\n bound = bind(typeof config === \"function\" ? await config() : config);\n return bound;\n });\n const instance = async (): Promise<Bound> => bound ?? build();\n\n // React's `cache` scopes the read to one server request, so a layout, a breadcrumb and a menu\n // asking in one render unseal the cookie and read the store once.\n const current = cache(async (): Promise<{ readonly session: Session | null; readonly url: URL }> => {\n const bound = await instance();\n const [jar, incoming] = await Promise.all([cookies(), headers()]);\n const url = renderedUrl(incoming);\n const found = await bound.party.read(jar.toString());\n if (found === null) return { session: null, url };\n return {\n session: withTenant(found, await bound.tenant({ url, headers: incoming, cookies: jar })),\n url,\n };\n });\n\n async function session(): Promise<Session | null>;\n async function session(options: { readonly required: true }): Promise<Session>;\n async function session(options?: { readonly required: true }): Promise<Session | null> {\n const { session: found, url } = await current();\n if (found !== null || options?.required !== true) return found;\n // AuthKit's `ensureSignedIn`: the proxy normally sends this browser to sign in before the page\n // renders, so this is the page the proxy does not run on, or a session that ended mid-render.\n const signIn = `${(await instance()).basePath}/signin`;\n redirect(`${signIn}?returnTo=${encodeURIComponent(`${url.pathname}${url.search}`)}`);\n }\n\n return {\n proxy: async (request, options) => gate(await instance(), request, options?.headers),\n routes: routes(instance),\n api: forward(instance),\n session,\n };\n}\n"],"names":["renderedUrl","incoming","forwarded","URL_HEADER","kanzoAuth","config","bound","build","singleFlight","bind","instance","current","cache","jar","cookies","headers","url","found","withTenant","session","options","signIn","redirect","request","gate","routes","forward"],"mappings":";;;;;;;;AA0EA,SAASA,EAAYC,GAAwB;AAC3C,QAAMC,IAAYD,EAAS,IAAIE,CAAU;AACzC,MAAID,MAAc;AAChB,QAAI;AACF,aAAO,IAAI,IAAIA,CAAS;AAAA,IAC1B,QAAQ;AAAA,IAER;AAEF,SAAO,IAAI,IAAI,WAAWD,EAAS,IAAI,MAAM,KAAK,WAAW,GAAG;AAClE;AAEO,SAASG,EACdC,GACW;AAGX,MAAIC;AACJ,QAAMC,IAAQC,EAAa,aACzBF,IAAQG,EAAK,OAAOJ,KAAW,aAAa,MAAMA,EAAA,IAAWA,CAAM,GAC5DC,EACR,GACKI,IAAW,YAA4BJ,KAASC,EAAA,GAIhDI,IAAUC,EAAM,YAA8E;AAClG,UAAMN,IAAQ,MAAMI,EAAA,GACd,CAACG,GAAKZ,CAAQ,IAAI,MAAM,QAAQ,IAAI,CAACa,EAAA,GAAWC,EAAA,CAAS,CAAC,GAC1DC,IAAMhB,EAAYC,CAAQ,GAC1BgB,IAAQ,MAAMX,EAAM,MAAM,KAAKO,EAAI,UAAU;AACnD,WAAII,MAAU,OAAa,EAAE,SAAS,MAAM,KAAAD,EAAA,IACrC;AAAA,MACL,SAASE,EAAWD,GAAO,MAAMX,EAAM,OAAO,EAAE,KAAAU,GAAK,SAASf,GAAU,SAASY,EAAA,CAAK,CAAC;AAAA,MACvF,KAAAG;AAAA,IAAA;AAAA,EAEJ,CAAC;AAID,iBAAeG,EAAQC,GAAgE;AACrF,UAAM,EAAE,SAASH,GAAO,KAAAD,EAAA,IAAQ,MAAML,EAAA;AACtC,QAAIM,MAAU,SAAQG,KAAA,gBAAAA,EAAS,cAAa,GAAM,QAAOH;AAGzD,UAAMI,IAAS,IAAI,MAAMX,EAAA,GAAY,QAAQ;AAC7C,IAAAY,EAAS,GAAGD,CAAM,aAAa,mBAAmB,GAAGL,EAAI,QAAQ,GAAGA,EAAI,MAAM,EAAE,CAAC,EAAE;AAAA,EACrF;AAEA,SAAO;AAAA,IACL,OAAO,OAAOO,GAASH,MAAYI,EAAK,MAAMd,KAAYa,GAASH,KAAA,gBAAAA,EAAS,OAAO;AAAA,IACnF,QAAQK,EAAOf,CAAQ;AAAA,IACvB,KAAKgB,EAAQhB,CAAQ;AAAA,IACrB,SAAAS;AAAA,EAAA;AAEJ;"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { RelyingParty, RelyingPartyConfig } from './server';
|
|
2
|
+
import { Session } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* A `kanzoAuth` config once read: one relying party, and the settings the proxy, the routes and
|
|
5
|
+
* the forwarder share. In a module of its own so those three can take it without importing the
|
|
6
|
+
* door that builds them.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* What a tenant resolver is handed. The proxy, the routes and a server component each build it
|
|
10
|
+
* from the request in front of them — a server component from the `x-kanzo-url` the proxy
|
|
11
|
+
* forwarded — so the same function answers the same way in all three.
|
|
12
|
+
*/
|
|
13
|
+
type TenantRequest = {
|
|
14
|
+
readonly url: URL;
|
|
15
|
+
readonly headers: Headers;
|
|
16
|
+
readonly cookies: {
|
|
17
|
+
get(name: string): {
|
|
18
|
+
readonly value: string;
|
|
19
|
+
} | undefined;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export interface KanzoAuthConfig extends RelyingPartyConfig {
|
|
23
|
+
/**
|
|
24
|
+
* The organization a request addresses: a hostname's first label, a path segment, a cookie, or
|
|
25
|
+
* one fixed alias for a deployment per tenant. Its answer is `Session.organization`, and the
|
|
26
|
+
* organization `can` asks inside by default. Absent, sessions carry no current tenant.
|
|
27
|
+
*/
|
|
28
|
+
readonly organization?: (request: TenantRequest) => string | undefined | Promise<string | undefined>;
|
|
29
|
+
/** Where `routes` is mounted. Default `/api/auth`. */
|
|
30
|
+
readonly basePath?: string;
|
|
31
|
+
/**
|
|
32
|
+
* The product's page for a failed sign-in, callback or sign-out, and for a session store that
|
|
33
|
+
* does not answer, reached as `?code=<AuthErrorCode>`. Default `/auth/problem`. Always public:
|
|
34
|
+
* whoever is sent there has no session, and sending them to sign in instead is a loop when
|
|
35
|
+
* signing in is what failed.
|
|
36
|
+
*/
|
|
37
|
+
readonly problemPage?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Path prefixes the proxy lets through without a session — a health probe, a legal notice.
|
|
40
|
+
* Matched on segment boundaries: `/health` opens `/health/live` and not `/healthcare`.
|
|
41
|
+
*/
|
|
42
|
+
readonly public?: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* Override the callback URL. Absent, it is derived from the request: its origin, the path the
|
|
45
|
+
* routes sit on, and `/callback`. Deriving it trusts `Host`, which is bounded — Keycloak refuses
|
|
46
|
+
* a `redirect_uri` it has not registered — but a deployment behind a proxy that rewrites the
|
|
47
|
+
* host says the URL out loud here.
|
|
48
|
+
*/
|
|
49
|
+
readonly redirectUri?: string;
|
|
50
|
+
/** Seconds of access-token lifetime below which a request renews it. Default 60. */
|
|
51
|
+
readonly renewWithin?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Forward `mount` to a resource server at `target` with the access token as a bearer — the
|
|
54
|
+
* token-mediating backend. The proxy leaves `mount` alone; `api` answers it.
|
|
55
|
+
*/
|
|
56
|
+
readonly api?: {
|
|
57
|
+
readonly mount: string;
|
|
58
|
+
readonly target: string;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** What a `kanzoAuth` instance is once its config has been read: one relying party and its settings. */
|
|
62
|
+
export interface Bound {
|
|
63
|
+
readonly party: RelyingParty;
|
|
64
|
+
readonly basePath: string;
|
|
65
|
+
readonly problemPage: string;
|
|
66
|
+
readonly redirectUri?: string;
|
|
67
|
+
readonly renewWithin?: number;
|
|
68
|
+
readonly api?: {
|
|
69
|
+
readonly mount: string;
|
|
70
|
+
readonly target: URL;
|
|
71
|
+
};
|
|
72
|
+
/** Prefixes the proxy lets through: the routes, the forwarder, the problem page, and `public`. */
|
|
73
|
+
readonly open: readonly string[];
|
|
74
|
+
tenant(request: TenantRequest): Promise<string | undefined>;
|
|
75
|
+
}
|
|
76
|
+
/** The request header the proxy forwards the URL on — AuthKit's `x-url` — for a server component to read. */
|
|
77
|
+
export declare const URL_HEADER = "x-kanzo-url";
|
|
78
|
+
export declare function bind(config: KanzoAuthConfig): Bound;
|
|
79
|
+
/** The current tenant on a session, when there is one. */
|
|
80
|
+
export declare function withTenant(session: Session, organization: string | undefined): Session;
|
|
81
|
+
export {};
|
|
82
|
+
//# sourceMappingURL=next-bound.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next-bound.d.ts","sourceRoot":"","sources":["../src/next-bound.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,YAAY,EAAE,KAAK,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACpF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGvC;;;;GAIG;AAEH;;;;GAIG;AACH,KAAK,aAAa,GAAG;IACnB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC;IAClB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE;QAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG;YAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;SAAE,GAAG,SAAS,CAAA;KAAE,CAAC;CACjF,CAAC;AAEF,MAAM,WAAW,eAAgB,SAAQ,kBAAkB;IACzD;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACrG,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,oFAAoF;IACpF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACpE;AAED,wGAAwG;AACxG,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,GAAG,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAA;KAAE,CAAC;IAChE,kGAAkG;IAClG,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAC7D;AAED,6GAA6G;AAC7G,eAAO,MAAM,UAAU,gBAAgB,CAAC;AAGxC,wBAAgB,IAAI,CAAC,MAAM,EAAE,eAAe,GAAG,KAAK,CAoBnD;AAED,0DAA0D;AAC1D,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEtF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { relyingParty as p } from "./server.js";
|
|
2
|
+
import { withoutTrailingSlashes as a } from "./issuer.js";
|
|
3
|
+
const d = "x-kanzo-url";
|
|
4
|
+
function h(t) {
|
|
5
|
+
const r = a(t.basePath ?? "/api/auth"), i = t.problemPage ?? "/auth/problem", e = t.api === void 0 ? void 0 : { mount: a(t.api.mount), target: new URL(t.api.target) }, n = t.organization;
|
|
6
|
+
return {
|
|
7
|
+
party: p(t),
|
|
8
|
+
basePath: r,
|
|
9
|
+
problemPage: i,
|
|
10
|
+
redirectUri: t.redirectUri,
|
|
11
|
+
renewWithin: t.renewWithin,
|
|
12
|
+
api: e,
|
|
13
|
+
open: [r, i, ...e === void 0 ? [] : [e.mount], ...t.public ?? []].map(
|
|
14
|
+
a
|
|
15
|
+
),
|
|
16
|
+
tenant: async (o) => n === void 0 ? void 0 : n(o)
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function l(t, r) {
|
|
20
|
+
return r === void 0 ? t : { ...t, organization: r };
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
d as URL_HEADER,
|
|
24
|
+
h as bind,
|
|
25
|
+
l as withTenant
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=next-bound.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next-bound.js","sources":["../src/next-bound.ts"],"sourcesContent":["import { relyingParty, type RelyingParty, type RelyingPartyConfig } from \"./server\";\nimport type { Session } from \"./types\";\nimport { withoutTrailingSlashes } from \"./issuer\";\n\n/**\n * A `kanzoAuth` config once read: one relying party, and the settings the proxy, the routes and\n * the forwarder share. In a module of its own so those three can take it without importing the\n * door that builds them.\n */\n\n/**\n * What a tenant resolver is handed. The proxy, the routes and a server component each build it\n * from the request in front of them — a server component from the `x-kanzo-url` the proxy\n * forwarded — so the same function answers the same way in all three.\n */\ntype TenantRequest = {\n readonly url: URL;\n readonly headers: Headers;\n readonly cookies: { get(name: string): { readonly value: string } | undefined };\n};\n\nexport interface KanzoAuthConfig extends RelyingPartyConfig {\n /**\n * The organization a request addresses: a hostname's first label, a path segment, a cookie, or\n * one fixed alias for a deployment per tenant. Its answer is `Session.organization`, and the\n * organization `can` asks inside by default. Absent, sessions carry no current tenant.\n */\n readonly organization?: (request: TenantRequest) => string | undefined | Promise<string | undefined>;\n /** Where `routes` is mounted. Default `/api/auth`. */\n readonly basePath?: string;\n /**\n * The product's page for a failed sign-in, callback or sign-out, and for a session store that\n * does not answer, reached as `?code=<AuthErrorCode>`. Default `/auth/problem`. Always public:\n * whoever is sent there has no session, and sending them to sign in instead is a loop when\n * signing in is what failed.\n */\n readonly problemPage?: string;\n /**\n * Path prefixes the proxy lets through without a session — a health probe, a legal notice.\n * Matched on segment boundaries: `/health` opens `/health/live` and not `/healthcare`.\n */\n readonly public?: readonly string[];\n /**\n * Override the callback URL. Absent, it is derived from the request: its origin, the path the\n * routes sit on, and `/callback`. Deriving it trusts `Host`, which is bounded — Keycloak refuses\n * a `redirect_uri` it has not registered — but a deployment behind a proxy that rewrites the\n * host says the URL out loud here.\n */\n readonly redirectUri?: string;\n /** Seconds of access-token lifetime below which a request renews it. Default 60. */\n readonly renewWithin?: number;\n /**\n * Forward `mount` to a resource server at `target` with the access token as a bearer — the\n * token-mediating backend. The proxy leaves `mount` alone; `api` answers it.\n */\n readonly api?: { readonly mount: string; readonly target: string };\n}\n\n/** What a `kanzoAuth` instance is once its config has been read: one relying party and its settings. */\nexport interface Bound {\n readonly party: RelyingParty;\n readonly basePath: string;\n readonly problemPage: string;\n readonly redirectUri?: string;\n readonly renewWithin?: number;\n readonly api?: { readonly mount: string; readonly target: URL };\n /** Prefixes the proxy lets through: the routes, the forwarder, the problem page, and `public`. */\n readonly open: readonly string[];\n tenant(request: TenantRequest): Promise<string | undefined>;\n}\n\n/** The request header the proxy forwards the URL on — AuthKit's `x-url` — for a server component to read. */\nexport const URL_HEADER = \"x-kanzo-url\";\n\n\nexport function bind(config: KanzoAuthConfig): Bound {\n const basePath = withoutTrailingSlashes(config.basePath ?? \"/api/auth\");\n const problemPage = config.problemPage ?? \"/auth/problem\";\n const api =\n config.api === undefined\n ? undefined\n : { mount: withoutTrailingSlashes(config.api.mount), target: new URL(config.api.target) };\n const resolve = config.organization;\n return {\n party: relyingParty(config),\n basePath,\n problemPage,\n redirectUri: config.redirectUri,\n renewWithin: config.renewWithin,\n api,\n open: [basePath, problemPage, ...(api === undefined ? [] : [api.mount]), ...(config.public ?? [])].map(\n withoutTrailingSlashes,\n ),\n tenant: async (request) => (resolve === undefined ? undefined : resolve(request)),\n };\n}\n\n/** The current tenant on a session, when there is one. */\nexport function withTenant(session: Session, organization: string | undefined): Session {\n return organization === undefined ? session : { ...session, organization };\n}\n\n"],"names":["URL_HEADER","bind","config","basePath","withoutTrailingSlashes","problemPage","api","resolve","relyingParty","request","withTenant","session","organization"],"mappings":";;AAwEO,MAAMA,IAAa;AAGnB,SAASC,EAAKC,GAAgC;AACnD,QAAMC,IAAWC,EAAuBF,EAAO,YAAY,WAAW,GAChEG,IAAcH,EAAO,eAAe,iBACpCI,IACJJ,EAAO,QAAQ,SACX,SACA,EAAE,OAAOE,EAAuBF,EAAO,IAAI,KAAK,GAAG,QAAQ,IAAI,IAAIA,EAAO,IAAI,MAAM,EAAA,GACpFK,IAAUL,EAAO;AACvB,SAAO;AAAA,IACL,OAAOM,EAAaN,CAAM;AAAA,IAC1B,UAAAC;AAAA,IACA,aAAAE;AAAA,IACA,aAAaH,EAAO;AAAA,IACpB,aAAaA,EAAO;AAAA,IACpB,KAAAI;AAAA,IACA,MAAM,CAACH,GAAUE,GAAa,GAAIC,MAAQ,SAAY,CAAA,IAAK,CAACA,EAAI,KAAK,GAAI,GAAIJ,EAAO,UAAU,CAAA,CAAG,EAAE;AAAA,MACjGE;AAAA,IAAA;AAAA,IAEF,QAAQ,OAAOK,MAAaF,MAAY,SAAY,SAAYA,EAAQE,CAAO;AAAA,EAAA;AAEnF;AAGO,SAASC,EAAWC,GAAkBC,GAA2C;AACtF,SAAOA,MAAiB,SAAYD,IAAU,EAAE,GAAGA,GAAS,cAAAC,EAAA;AAC9D;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next-gate.d.ts","sourceRoot":"","sources":["../src/next-gate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAc,KAAK,KAAK,EAAE,MAAM,cAAc,CAAC;AA6FtD,wBAAsB,IAAI,CACxB,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,WAAW,EACpB,KAAK,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC,YAAY,CAAC,CAmDvB"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { NextResponse as h } from "next/server";
|
|
2
|
+
import { URL_HEADER as m } from "./next-bound.js";
|
|
3
|
+
import { outage as p } from "./next-routes.js";
|
|
4
|
+
import { AuthError as k } from "./types.js";
|
|
5
|
+
function w(t) {
|
|
6
|
+
return t.slice(t.lastIndexOf("/") + 1).includes(".");
|
|
7
|
+
}
|
|
8
|
+
function g(t, r) {
|
|
9
|
+
return r.some((a) => t === a || t.startsWith(`${a}/`));
|
|
10
|
+
}
|
|
11
|
+
function b(t) {
|
|
12
|
+
const [r = "", ...a] = t.split(";").map((o) => o.trim()), c = r.indexOf("="), n = { name: r.slice(0, c), value: r.slice(c + 1) };
|
|
13
|
+
for (const o of a) {
|
|
14
|
+
const [l = "", i = ""] = o.split("=");
|
|
15
|
+
switch (l.toLowerCase()) {
|
|
16
|
+
case "path":
|
|
17
|
+
n.path = i;
|
|
18
|
+
break;
|
|
19
|
+
case "httponly":
|
|
20
|
+
n.httpOnly = !0;
|
|
21
|
+
break;
|
|
22
|
+
case "secure":
|
|
23
|
+
n.secure = !0;
|
|
24
|
+
break;
|
|
25
|
+
case "samesite":
|
|
26
|
+
n.sameSite = i.toLowerCase();
|
|
27
|
+
break;
|
|
28
|
+
case "max-age":
|
|
29
|
+
n.maxAge = Number(i);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return n;
|
|
34
|
+
}
|
|
35
|
+
async function v(t, r, a) {
|
|
36
|
+
const { pathname: c, search: n } = r.nextUrl, o = new Headers(r.headers);
|
|
37
|
+
for (const e of [...o.keys()])
|
|
38
|
+
e.startsWith("x-kanzo-") && o.delete(e);
|
|
39
|
+
o.set(m, r.url), new Headers(a).forEach((e, s) => o.set(s, e));
|
|
40
|
+
const l = () => h.next({ request: { headers: o } });
|
|
41
|
+
if (w(c) || g(c, t.open)) return l();
|
|
42
|
+
const i = r.headers.get("sec-fetch-mode"), u = i === null || i === "navigate";
|
|
43
|
+
let f;
|
|
44
|
+
try {
|
|
45
|
+
f = await t.party.token(r.headers.get("cookie"), { renewWithin: t.renewWithin });
|
|
46
|
+
} catch (e) {
|
|
47
|
+
if (!(e instanceof k)) throw e;
|
|
48
|
+
if (e.code === "idp/unreachable" || e.code === "idp/silent") return l();
|
|
49
|
+
if (!u)
|
|
50
|
+
return new h(null, {
|
|
51
|
+
status: p(e.code) ?? 500,
|
|
52
|
+
headers: { "cache-control": "no-store" }
|
|
53
|
+
});
|
|
54
|
+
const s = new URL(t.problemPage, r.url);
|
|
55
|
+
return s.searchParams.set("code", e.code), h.redirect(s, 307);
|
|
56
|
+
}
|
|
57
|
+
if (f.ended) {
|
|
58
|
+
let e;
|
|
59
|
+
if (u) {
|
|
60
|
+
const s = new URL(`${t.basePath}/signin`, r.url);
|
|
61
|
+
s.searchParams.set("returnTo", `${c}${n}`), e = h.redirect(s, 307);
|
|
62
|
+
} else
|
|
63
|
+
e = new h(null, { status: 401, headers: { "cache-control": "no-store" } });
|
|
64
|
+
for (const s of f.cookies) e.headers.append("set-cookie", s);
|
|
65
|
+
return e;
|
|
66
|
+
}
|
|
67
|
+
const d = l();
|
|
68
|
+
for (const e of f.cookies) d.cookies.set(b(e));
|
|
69
|
+
return d;
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
v as gate
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=next-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next-gate.js","sources":["../src/next-gate.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from \"next/server\";\nimport { URL_HEADER, type Bound } from \"./next-bound\";\nimport { outage } from \"./next-routes\";\nimport type { Ended, Token } from \"./server\";\nimport { AuthError } from \"./types\";\n\n/**\n * The proxy as the session authority: WorkOS AuthKit's `authkitProxy`, which renews and enforces\n * where the URL is known, over a Duende-style session that is updated in place.\n *\n * Next 16 runs `proxy.ts` on Node and only on Node, so the edge's reason for checking a cookie's\n * mere presence is gone. This reads the record — one store read per request — and acts on it\n * before any page renders:\n *\n * - **A live session within a minute of expiry is renewed.** Under a ticket store the tokens are\n * rewritten under the same ticket and the cookie does not change. Under the stateless store the\n * re-sealed cookie is set with `response.cookies.set`, which Next also merges into this same\n * request's `cookies()`, so the page renders with the session the browser will hold.\n * - **No live session** — none, forgotten by the store, or refused by the IdP, which is a realm\n * whose SSO session went idle — **clears the cookie and asks for a sign-in.** A navigation is\n * sent to `signin?returnTo=<path+search>`; anything else answers a bare 401, because a prefetch,\n * a server action or an RSC fetch cannot follow a sign-in and must not start one. The Next\n * router answers a 401 to its own fetch with a hard navigation, which comes back through here as\n * a navigation.\n * - **A store that does not answer** sends a navigation to the problem page with its code; **an\n * IdP that does not answer** is passed through untouched, because the session it could not renew\n * is still the person's, and a page that renders is better than a sign-in that cannot finish.\n *\n * ## Navigation or not, by Fetch Metadata\n *\n * `Sec-Fetch-Mode` (W3C Fetch Metadata) is set by the browser and cannot be set by script:\n * `navigate` is a document load, `cors`/`no-cors`/`same-origin` is everything else. Next strips its\n * own RSC headers before the proxy sees them, so this is the one signal that survives. An absent\n * header is a client that is not a browser, or one too old to send it, and is treated as a\n * navigation — a redirect it can follow is the more useful answer.\n *\n * ## The URL, forwarded\n *\n * Every request leaves with `x-kanzo-url` set to the URL it arrived at — AuthKit's `x-url` — and\n * with every incoming `x-kanzo-*` header deleted first, so a browser cannot choose what a server\n * component reads there. `session({ required: true })` signs in back to it, and the tenant\n * resolver reads the same URL the routes do.\n */\n\n/** A path whose last segment carries a dot: a static file, not a page. */\nfunction isFile(pathname: string): boolean {\n return pathname.slice(pathname.lastIndexOf(\"/\") + 1).includes(\".\");\n}\n\n/** On a segment boundary, so `/health` opens `/health/live` and not `/healthcare`. */\nfunction under(pathname: string, prefixes: readonly string[]): boolean {\n return prefixes.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`));\n}\n\n/**\n * A `Set-Cookie` value as the attributes `response.cookies.set` takes, which is the call that makes\n * Next merge the cookie into this request's `cookies()`. A raw `Set-Cookie` header reaches the\n * browser and not the page being rendered.\n */\nfunction asCookie(header: string) {\n const [pair = \"\", ...attributes] = header.split(\";\").map((part) => part.trim());\n const eq = pair.indexOf(\"=\");\n const cookie: {\n name: string;\n value: string;\n path?: string;\n httpOnly?: boolean;\n secure?: boolean;\n sameSite?: \"lax\" | \"strict\" | \"none\";\n maxAge?: number;\n } = { name: pair.slice(0, eq), value: pair.slice(eq + 1) };\n for (const attribute of attributes) {\n const [key = \"\", value = \"\"] = attribute.split(\"=\");\n switch (key.toLowerCase()) {\n case \"path\":\n cookie.path = value;\n break;\n case \"httponly\":\n cookie.httpOnly = true;\n break;\n case \"secure\":\n cookie.secure = true;\n break;\n case \"samesite\":\n cookie.sameSite = value.toLowerCase() as \"lax\" | \"strict\" | \"none\";\n break;\n case \"max-age\":\n cookie.maxAge = Number(value);\n break;\n }\n }\n return cookie;\n}\n\nexport async function gate(\n bound: Bound,\n request: NextRequest,\n extra?: HeadersInit,\n): Promise<NextResponse> {\n const { pathname, search } = request.nextUrl;\n\n const forwarded = new Headers(request.headers);\n for (const name of [...forwarded.keys()]) {\n if (name.startsWith(\"x-kanzo-\")) forwarded.delete(name);\n }\n forwarded.set(URL_HEADER, request.url);\n new Headers(extra).forEach((value, name) => forwarded.set(name, value));\n const through = () => NextResponse.next({ request: { headers: forwarded } });\n\n if (isFile(pathname) || under(pathname, bound.open)) return through();\n\n const mode = request.headers.get(\"sec-fetch-mode\");\n const navigation = mode === null || mode === \"navigate\";\n\n let held: Token | Ended;\n try {\n held = await bound.party.token(request.headers.get(\"cookie\"), { renewWithin: bound.renewWithin });\n } catch (error) {\n if (!(error instanceof AuthError)) throw error;\n if (error.code === \"idp/unreachable\" || error.code === \"idp/silent\") return through();\n if (!navigation) {\n return new NextResponse(null, {\n status: outage(error.code) ?? 500,\n headers: { \"cache-control\": \"no-store\" },\n });\n }\n const problem = new URL(bound.problemPage, request.url);\n problem.searchParams.set(\"code\", error.code);\n return NextResponse.redirect(problem, 307);\n }\n\n if (held.ended) {\n let answer: NextResponse;\n if (navigation) {\n const signIn = new URL(`${bound.basePath}/signin`, request.url);\n // `routes` confines `returnTo` to this origin before sealing it; this relies on that check\n // rather than repeating it.\n signIn.searchParams.set(\"returnTo\", `${pathname}${search}`);\n answer = NextResponse.redirect(signIn, 307);\n } else {\n answer = new NextResponse(null, { status: 401, headers: { \"cache-control\": \"no-store\" } });\n }\n for (const cookie of held.cookies) answer.headers.append(\"set-cookie\", cookie);\n return answer;\n }\n\n const response = through();\n for (const cookie of held.cookies) response.cookies.set(asCookie(cookie));\n return response;\n}\n"],"names":["isFile","pathname","under","prefixes","prefix","asCookie","header","pair","attributes","part","eq","cookie","attribute","key","value","gate","bound","request","extra","search","forwarded","name","URL_HEADER","through","NextResponse","mode","navigation","held","error","AuthError","outage","problem","answer","signIn","response"],"mappings":";;;;AA6CA,SAASA,EAAOC,GAA2B;AACzC,SAAOA,EAAS,MAAMA,EAAS,YAAY,GAAG,IAAI,CAAC,EAAE,SAAS,GAAG;AACnE;AAGA,SAASC,EAAMD,GAAkBE,GAAsC;AACrE,SAAOA,EAAS,KAAK,CAACC,MAAWH,MAAaG,KAAUH,EAAS,WAAW,GAAGG,CAAM,GAAG,CAAC;AAC3F;AAOA,SAASC,EAASC,GAAgB;AAChC,QAAM,CAACC,IAAO,IAAI,GAAGC,CAAU,IAAIF,EAAO,MAAM,GAAG,EAAE,IAAI,CAACG,MAASA,EAAK,MAAM,GACxEC,IAAKH,EAAK,QAAQ,GAAG,GACrBI,IAQF,EAAE,MAAMJ,EAAK,MAAM,GAAGG,CAAE,GAAG,OAAOH,EAAK,MAAMG,IAAK,CAAC,EAAA;AACvD,aAAWE,KAAaJ,GAAY;AAClC,UAAM,CAACK,IAAM,IAAIC,IAAQ,EAAE,IAAIF,EAAU,MAAM,GAAG;AAClD,YAAQC,EAAI,eAAY;AAAA,MACtB,KAAK;AACH,QAAAF,EAAO,OAAOG;AACd;AAAA,MACF,KAAK;AACH,QAAAH,EAAO,WAAW;AAClB;AAAA,MACF,KAAK;AACH,QAAAA,EAAO,SAAS;AAChB;AAAA,MACF,KAAK;AACH,QAAAA,EAAO,WAAWG,EAAM,YAAA;AACxB;AAAA,MACF,KAAK;AACH,QAAAH,EAAO,SAAS,OAAOG,CAAK;AAC5B;AAAA,IAAA;AAAA,EAEN;AACA,SAAOH;AACT;AAEA,eAAsBI,EACpBC,GACAC,GACAC,GACuB;AACvB,QAAM,EAAE,UAAAjB,GAAU,QAAAkB,EAAA,IAAWF,EAAQ,SAE/BG,IAAY,IAAI,QAAQH,EAAQ,OAAO;AAC7C,aAAWI,KAAQ,CAAC,GAAGD,EAAU,KAAA,CAAM;AACrC,IAAIC,EAAK,WAAW,UAAU,KAAGD,EAAU,OAAOC,CAAI;AAExD,EAAAD,EAAU,IAAIE,GAAYL,EAAQ,GAAG,GACrC,IAAI,QAAQC,CAAK,EAAE,QAAQ,CAACJ,GAAOO,MAASD,EAAU,IAAIC,GAAMP,CAAK,CAAC;AACtE,QAAMS,IAAU,MAAMC,EAAa,KAAK,EAAE,SAAS,EAAE,SAASJ,EAAA,GAAa;AAE3E,MAAIpB,EAAOC,CAAQ,KAAKC,EAAMD,GAAUe,EAAM,IAAI,EAAG,QAAOO,EAAA;AAE5D,QAAME,IAAOR,EAAQ,QAAQ,IAAI,gBAAgB,GAC3CS,IAAaD,MAAS,QAAQA,MAAS;AAE7C,MAAIE;AACJ,MAAI;AACF,IAAAA,IAAO,MAAMX,EAAM,MAAM,MAAMC,EAAQ,QAAQ,IAAI,QAAQ,GAAG,EAAE,aAAaD,EAAM,aAAa;AAAA,EAClG,SAASY,GAAO;AACd,QAAI,EAAEA,aAAiBC,GAAY,OAAMD;AACzC,QAAIA,EAAM,SAAS,qBAAqBA,EAAM,SAAS,qBAAqBL,EAAA;AAC5E,QAAI,CAACG;AACH,aAAO,IAAIF,EAAa,MAAM;AAAA,QAC5B,QAAQM,EAAOF,EAAM,IAAI,KAAK;AAAA,QAC9B,SAAS,EAAE,iBAAiB,WAAA;AAAA,MAAW,CACxC;AAEH,UAAMG,IAAU,IAAI,IAAIf,EAAM,aAAaC,EAAQ,GAAG;AACtD,WAAAc,EAAQ,aAAa,IAAI,QAAQH,EAAM,IAAI,GACpCJ,EAAa,SAASO,GAAS,GAAG;AAAA,EAC3C;AAEA,MAAIJ,EAAK,OAAO;AACd,QAAIK;AACJ,QAAIN,GAAY;AACd,YAAMO,IAAS,IAAI,IAAI,GAAGjB,EAAM,QAAQ,WAAWC,EAAQ,GAAG;AAG9D,MAAAgB,EAAO,aAAa,IAAI,YAAY,GAAGhC,CAAQ,GAAGkB,CAAM,EAAE,GAC1Da,IAASR,EAAa,SAASS,GAAQ,GAAG;AAAA,IAC5C;AACE,MAAAD,IAAS,IAAIR,EAAa,MAAM,EAAE,QAAQ,KAAK,SAAS,EAAE,iBAAiB,WAAA,GAAc;AAE3F,eAAWb,KAAUgB,EAAK,WAAgB,QAAQ,OAAO,cAAchB,CAAM;AAC7E,WAAOqB;AAAA,EACT;AAEA,QAAME,IAAWX,EAAA;AACjB,aAAWZ,KAAUgB,EAAK,QAAS,CAAAO,EAAS,QAAQ,IAAI7B,EAASM,CAAM,CAAC;AACxE,SAAOuB;AACT;"}
|