@manta-eu/auth 0.12.0 → 0.13.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.
@@ -0,0 +1,4 @@
1
+ export declare const crossSiteSignOutMessage = "Sign out from the app";
2
+ export declare function isWebSocket(request: Request): boolean;
3
+ export declare function originAllowsRequest(request: Request, ownOrigin?: string): boolean;
4
+ //# sourceMappingURL=request-origin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-origin.d.ts","sourceRoot":"","sources":["../src/request-origin.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,uBAAuB,0BAA0B,CAAC;AAE/D,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAErD;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,SAA8B,GAAG,OAAO,CAYtG"}
@@ -0,0 +1,17 @@
1
+ import { isPresent } from "@manta-eu/lib/present";
2
+ export const crossSiteSignOutMessage = "Sign out from the app";
3
+ export function isWebSocket(request) {
4
+ return request.headers.get("upgrade")?.toLowerCase() === "websocket";
5
+ }
6
+ export function originAllowsRequest(request, ownOrigin = new URL(request.url).origin) {
7
+ const fetchSite = request.headers.get("sec-fetch-site");
8
+ if (fetchSite === "same-origin")
9
+ return true;
10
+ if (fetchSite === "cross-site" || fetchSite === "same-site")
11
+ return false;
12
+ const origin = request.headers.get("origin");
13
+ if (isPresent(origin))
14
+ return origin === ownOrigin;
15
+ return !isWebSocket(request) && (request.method === "GET" || request.method === "HEAD");
16
+ }
17
+ //# sourceMappingURL=request-origin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-origin.js","sourceRoot":"","sources":["../src/request-origin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD,MAAM,CAAC,MAAM,uBAAuB,GAAG,uBAAuB,CAAC;AAE/D,MAAM,UAAU,WAAW,CAAC,OAAgB;IAC1C,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,KAAK,WAAW,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,OAAgB,EAAE,SAAS,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM;IAC3F,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAExD,IAAI,SAAS,KAAK,aAAa;QAAE,OAAO,IAAI,CAAC;IAE7C,IAAI,SAAS,KAAK,YAAY,IAAI,SAAS,KAAK,WAAW;QAAE,OAAO,KAAK,CAAC;IAE1E,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAE7C,IAAI,SAAS,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,KAAK,SAAS,CAAC;IAEnD,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,KAAK,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC;AAC1F,CAAC"}
@@ -0,0 +1,43 @@
1
+ import { type JWTPayload } from "jose";
2
+ export type SealingKey = Awaited<ReturnType<typeof crypto.subtle.deriveKey>>;
3
+ export declare function purposeKey(secret: string, purpose: string): Promise<SealingKey>;
4
+ export type SealArgs = {
5
+ key: SealingKey;
6
+ value: JWTPayload;
7
+ ttlMs?: number;
8
+ };
9
+ export declare function seal(args: SealArgs): Promise<string>;
10
+ export declare function unseal(key: SealingKey, value: string | undefined): Promise<JWTPayload | undefined>;
11
+ export declare function cookiesOf(request: Request): Map<string, string>;
12
+ export type ReturnToArgs = {
13
+ raw: string | null;
14
+ fallback: string;
15
+ appUrl: URL;
16
+ };
17
+ export declare function safeReturnTo(args: ReturnToArgs): string;
18
+ export declare const sessionMaxAgeSeconds: number;
19
+ export type CookieArgs = {
20
+ name: string;
21
+ value: string;
22
+ path?: string;
23
+ maxAge?: number;
24
+ };
25
+ export type SetCookie = (cookie: CookieArgs) => string;
26
+ export declare function httpOnlyCookie(secure: boolean): SetCookie;
27
+ export type ChunkCookiesArgs = {
28
+ cookies: Map<string, string>;
29
+ name: string;
30
+ setCookie: SetCookie;
31
+ };
32
+ export type SealedChunkCookiesArgs = ChunkCookiesArgs & {
33
+ key: SealingKey;
34
+ value: JWTPayload;
35
+ maxAge?: number;
36
+ };
37
+ export declare function sealedChunkCookies(args: SealedChunkCookiesArgs): Promise<string[]>;
38
+ export declare function expiredChunkCookies(args: ChunkCookiesArgs): string[];
39
+ export type UnsealedChunksArgs = Pick<ChunkCookiesArgs, "cookies" | "name"> & {
40
+ key: SealingKey;
41
+ };
42
+ export declare function unsealedChunks(args: UnsealedChunksArgs): Promise<JWTPayload | undefined>;
43
+ //# sourceMappingURL=sealed-cookies.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sealed-cookies.d.ts","sourceRoot":"","sources":["../src/sealed-cookies.ts"],"names":[],"mappings":"AACA,OAAO,EAA0B,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAE/D,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;AAO7E,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAKrF;AAED,MAAM,MAAM,QAAQ,GAAG;IAAE,GAAG,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,UAAU,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9E,wBAAsB,IAAI,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ1D;AAED,wBAAsB,MAAM,CAAC,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAUxG;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAQ/D;AAED,MAAM,MAAM,YAAY,GAAG;IAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC;AAEjF,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAUvD;AAgCD,eAAO,MAAM,oBAAoB,QAAoB,CAAC;AAEtD,MAAM,MAAM,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzF,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,UAAU,KAAK,MAAM,CAAC;AAEvD,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,CAazD;AAED,MAAM,MAAM,gBAAgB,GAAG;IAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,SAAS,CAAA;CAAE,CAAC;AAEpG,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,GAAG;IAAE,GAAG,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,UAAU,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAUxF;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,EAAE,CAIpE;AAED,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,MAAM,CAAC,GAAG;IAAE,GAAG,EAAE,UAAU,CAAA;CAAE,CAAC;AAElG,wBAAsB,cAAc,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAI9F"}
@@ -0,0 +1,95 @@
1
+ import { isPresent } from "@manta-eu/lib/present";
2
+ import { EncryptJWT, jwtDecrypt } from "jose";
3
+ const chunkSize = 3800;
4
+ const maxChunks = 10;
5
+ const encoded = (text) => new TextEncoder().encode(text);
6
+ export async function purposeKey(secret, purpose) {
7
+ const material = await crypto.subtle.importKey("raw", encoded(secret), "HKDF", false, ["deriveKey"]);
8
+ const derivation = { name: "HKDF", hash: "SHA-256", salt: encoded("manta-web-login"), info: encoded(purpose) };
9
+ return crypto.subtle.deriveKey(derivation, material, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
10
+ }
11
+ export async function seal(args) {
12
+ const { key, value, ttlMs } = args;
13
+ const jwt = new EncryptJWT({ ...value }).setProtectedHeader({ alg: "dir", enc: "A256GCM" });
14
+ if (ttlMs !== undefined)
15
+ jwt.setExpirationTime(new Date(Date.now() + ttlMs));
16
+ return jwt.encrypt(key);
17
+ }
18
+ export async function unseal(key, value) {
19
+ if (!isPresent(value))
20
+ return undefined;
21
+ try {
22
+ const { payload } = await jwtDecrypt(value, key);
23
+ return payload;
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ export function cookiesOf(request) {
30
+ const pairs = (request.headers.get("cookie") ?? "").split(";").flatMap((part) => {
31
+ const separator = part.indexOf("=");
32
+ return separator > 0 ? [[part.slice(0, separator).trim(), part.slice(separator + 1).trim()]] : [];
33
+ });
34
+ return new Map(pairs);
35
+ }
36
+ export function safeReturnTo(args) {
37
+ const { raw, fallback, appUrl } = args;
38
+ if (!raw?.startsWith("/") || /[\p{Cc}\\]/u.test(raw))
39
+ return fallback;
40
+ const { origin, pathname, search, hash } = new URL(raw, appUrl);
41
+ if (origin !== appUrl.origin || pathname.startsWith("//"))
42
+ return fallback;
43
+ return `${pathname}${search}${hash}`;
44
+ }
45
+ function cookieChunks(value) {
46
+ const count = Math.ceil(value.length / chunkSize);
47
+ if (count > maxChunks)
48
+ throw new Error("The session is too large to keep in cookies");
49
+ return Array.from({ length: count }, (_, index) => value.slice(index * chunkSize, (index + 1) * chunkSize));
50
+ }
51
+ function joinedChunks(cookies, name) {
52
+ const names = Array.from({ length: maxChunks }, (_, index) => `${name}.${index}`);
53
+ const firstMissing = names.findIndex((chunk) => !cookies.has(chunk));
54
+ return names
55
+ .slice(0, firstMissing === -1 ? undefined : firstMissing)
56
+ .map((chunk) => cookies.get(chunk))
57
+ .join("");
58
+ }
59
+ function chunkNamesFrom(args) {
60
+ const { cookies, name, first } = args;
61
+ const prefix = `${name}.`;
62
+ return [...cookies.keys()].filter((cookie) => cookie.startsWith(prefix) && Number(cookie.slice(prefix.length)) >= first);
63
+ }
64
+ export const sessionMaxAgeSeconds = 30 * 24 * 60 * 60;
65
+ export function httpOnlyCookie(secure) {
66
+ return (cookie) => {
67
+ const { name, value, path = "/", maxAge } = cookie;
68
+ return [
69
+ `${name}=${value}`,
70
+ `Path=${path}`,
71
+ "HttpOnly",
72
+ "SameSite=Lax",
73
+ ...(secure ? ["Secure"] : []),
74
+ ...(maxAge === undefined ? [] : [`Max-Age=${maxAge}`]),
75
+ ].join("; ");
76
+ };
77
+ }
78
+ export async function sealedChunkCookies(args) {
79
+ const { cookies, name, setCookie, key, value, maxAge } = args;
80
+ const chunks = cookieChunks(await seal({ key, value, ttlMs: maxAge === undefined ? undefined : maxAge * 1000 }));
81
+ const stale = chunkNamesFrom({ cookies, name, first: chunks.length });
82
+ return [
83
+ ...chunks.map((chunk, index) => setCookie({ name: `${name}.${index}`, value: chunk, maxAge })),
84
+ ...stale.map((chunk) => setCookie({ name: chunk, value: "", maxAge: 0 })),
85
+ ];
86
+ }
87
+ export function expiredChunkCookies(args) {
88
+ const { cookies, name, setCookie } = args;
89
+ return chunkNamesFrom({ cookies, name, first: 0 }).map((chunk) => setCookie({ name: chunk, value: "", maxAge: 0 }));
90
+ }
91
+ export async function unsealedChunks(args) {
92
+ const { cookies, name, key } = args;
93
+ return unseal(key, joinedChunks(cookies, name));
94
+ }
95
+ //# sourceMappingURL=sealed-cookies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sealed-cookies.js","sourceRoot":"","sources":["../src/sealed-cookies.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"web-routes.d.ts","sourceRoot":"","sources":["../src/web-routes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAW5C,MAAM,MAAM,qBAAqB,GAAG;IAClC,gHAAgH;IAChH,MAAM,EAAE,UAAU,GAAG;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,uHAAuH;IACvH,MAAM,EAAE,MAAM,CAAC;IACf,kFAAkF;IAClF,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/D,yGAAyG;IACzG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4FAA4F;IAC5F,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2GAA2G;IAC3G,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,uFAAuF;IACvF,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC;IACxD,+EAA+E;IAC/E,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CAChD,CAAC;AAmFF,yGAAyG;AACzG,wBAAgB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,cAAc,CA+L7E"}
1
+ {"version":3,"file":"web-routes.d.ts","sourceRoot":"","sources":["../src/web-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAwB5C,MAAM,MAAM,qBAAqB,GAAG;IAClC,gHAAgH;IAChH,MAAM,EAAE,UAAU,GAAG;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,uHAAuH;IACvH,MAAM,EAAE,MAAM,CAAC;IACf,kFAAkF;IAClF,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/D,yGAAyG;IACzG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4FAA4F;IAC5F,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2GAA2G;IAC3G,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,uFAAuF;IACvF,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC;IACxD,+EAA+E;IAC/E,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CAChD,CAAC;AA+BF,yGAAyG;AACzG,wBAAgB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,cAAc,CAuM7E"}
@@ -1,57 +1,9 @@
1
- import { EncryptJWT, jwtDecrypt } from "jose";
1
+ import { crossSiteSignOutMessage, originAllowsRequest } from "./request-origin.js";
2
+ import { cookiesOf, expiredChunkCookies, purposeKey, safeReturnTo, seal, sealedChunkCookies, sessionMaxAgeSeconds, unseal, unsealedChunks, httpOnlyCookie, } from "./sealed-cookies.js";
2
3
  import { accessTokenExpiresAt } from "./token.js";
3
4
  import { endWebLogin, finishWebLogin, refreshWebLogin, startWebLogin, webLogoutUrl, } from "./web-login.js";
4
5
  const loginTtlMs = 10 * 60 * 1000;
5
- const offlineCookieMaxAgeSeconds = 30 * 24 * 60 * 60;
6
- const chunkSize = 3800;
7
- const maxChunks = 10;
8
6
  const refreshReuseMs = 10_000;
9
- // One key per purpose, so a login cookie can never be replayed as a session cookie or the other way round.
10
- async function purposeKey(secret, purpose) {
11
- const material = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), "HKDF", false, ["deriveKey"]);
12
- return crypto.subtle.deriveKey({
13
- name: "HKDF",
14
- hash: "SHA-256",
15
- salt: new TextEncoder().encode("manta-web-login"),
16
- info: new TextEncoder().encode(purpose),
17
- }, material, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
18
- }
19
- async function seal(key, value, ttlMs) {
20
- const jwt = new EncryptJWT({ ...value }).setProtectedHeader({ alg: "dir", enc: "A256GCM" });
21
- if (ttlMs !== undefined)
22
- jwt.setExpirationTime(new Date(Date.now() + ttlMs));
23
- return jwt.encrypt(key);
24
- }
25
- async function unseal(key, value) {
26
- if (!value)
27
- return undefined;
28
- try {
29
- return (await jwtDecrypt(value, key)).payload;
30
- }
31
- catch {
32
- return undefined;
33
- }
34
- }
35
- function cookiesOf(request) {
36
- const cookies = new Map();
37
- for (const part of (request.headers.get("cookie") ?? "").split(";")) {
38
- const separator = part.indexOf("=");
39
- if (separator > 0)
40
- cookies.set(part.slice(0, separator).trim(), part.slice(separator + 1).trim());
41
- }
42
- return cookies;
43
- }
44
- // Only a path on the app itself, so `/login?returnTo=` can never send people to another site.
45
- // A browser drops tabs and newlines from a Location, so `/\t/evil.example` would land on `//evil.example`.
46
- function safeReturnTo(raw, fallback, appUrl) {
47
- if (!raw?.startsWith("/") || /[\p{Cc}\\]/u.test(raw))
48
- return fallback;
49
- const resolved = new URL(raw, appUrl);
50
- // Resolving drops dot segments, so `/.//evil.example` comes out as the protocol-relative `//evil.example`.
51
- if (resolved.origin !== appUrl.origin || resolved.pathname.startsWith("//"))
52
- return fallback;
53
- return `${resolved.pathname}${resolved.search}${resolved.hash}`;
54
- }
55
7
  function isStoredSession(value) {
56
8
  return typeof value === "object" && value !== null && typeof value.accessToken === "string";
57
9
  }
@@ -72,7 +24,7 @@ export function webLoginRoutes(options) {
72
24
  if (options.sessionSecret.length < 32)
73
25
  throw new Error("sessionSecret must be at least 32 characters");
74
26
  const appUrl = new URL(options.appUrl);
75
- const secure = appUrl.protocol === "https:";
27
+ const cookie = httpOnlyCookie(appUrl.protocol === "https:");
76
28
  const paths = { login: "/login", callback: "/callback", logout: "/logout", ...options.paths };
77
29
  const afterLogin = options.afterLogin ?? "/";
78
30
  const afterLogout = options.afterLogout ?? "/";
@@ -114,55 +66,40 @@ export function webLoginRoutes(options) {
114
66
  }
115
67
  }
116
68
  }
117
- function cookie(name, value, extra = {}) {
118
- const attributes = [`${name}=${value}`, `Path=${extra.path ?? "/"}`, "HttpOnly", "SameSite=Lax"];
119
- if (secure)
120
- attributes.push("Secure");
121
- if (extra.maxAge !== undefined)
122
- attributes.push(`Max-Age=${extra.maxAge}`);
123
- return attributes.join("; ");
124
- }
125
69
  function clearedSession(cookies) {
126
- return [...cookies.keys()]
127
- .filter((name) => name.startsWith(`${sessionName}.`))
128
- .map((name) => cookie(name, "", { maxAge: 0 }));
70
+ return expiredChunkCookies({ cookies, name: sessionName, setCookie: cookie });
129
71
  }
130
72
  async function sessionCookies(stored, cookies) {
131
- const value = await seal(await keys.session, stored);
132
- const chunks = Math.ceil(value.length / chunkSize);
133
- if (chunks > maxChunks)
134
- throw new Error("The session is too large to keep in cookies");
135
- const maxAge = options.offlineAccess === true ? offlineCookieMaxAgeSeconds : undefined;
136
- const written = Array.from({ length: chunks }, (_, index) => cookie(`${sessionName}.${index}`, value.slice(index * chunkSize, (index + 1) * chunkSize), {
137
- ...(maxAge === undefined ? {} : { maxAge }),
138
- }));
139
- const stale = [...cookies.keys()]
140
- .filter((name) => name.startsWith(`${sessionName}.`) && Number(name.slice(sessionName.length + 1)) >= chunks)
141
- .map((name) => cookie(name, "", { maxAge: 0 }));
142
- return [...written, ...stale];
73
+ const maxAge = options.offlineAccess === true ? sessionMaxAgeSeconds : undefined;
74
+ return sealedChunkCookies({
75
+ cookies,
76
+ name: sessionName,
77
+ setCookie: cookie,
78
+ key: await keys.session,
79
+ value: stored,
80
+ maxAge,
81
+ });
143
82
  }
144
83
  async function storedSession(cookies) {
145
- let value = "";
146
- for (let index = 0; cookies.has(`${sessionName}.${index}`); index += 1) {
147
- value += cookies.get(`${sessionName}.${index}`);
148
- }
149
- const stored = await unseal(await keys.session, value);
84
+ const stored = await unsealedChunks({ cookies, name: sessionName, key: await keys.session });
150
85
  return isStoredSession(stored) ? stored : undefined;
151
86
  }
152
87
  async function login(request) {
153
- const returnTo = safeReturnTo(new URL(request.url).searchParams.get("returnTo"), afterLogin, appUrl);
88
+ const raw = new URL(request.url).searchParams.get("returnTo");
89
+ const returnTo = safeReturnTo({ raw, fallback: afterLogin, appUrl });
154
90
  const { url, state, verifier } = await startWebLogin(client, {
155
91
  redirectUri,
156
92
  ...(options.offlineAccess === true ? { offlineAccess: true } : {}),
157
93
  ...(options.idpHint ? { idpHint: options.idpHint } : {}),
158
94
  });
159
- const sealed = await seal(await keys.login, { state, verifier, returnTo }, loginTtlMs);
160
- return redirect(url, [cookie(loginName, sealed, { path: paths.callback, maxAge: loginTtlMs / 1000 })]);
95
+ const value = { state, verifier, returnTo };
96
+ const sealed = await seal({ key: await keys.login, value, ttlMs: loginTtlMs });
97
+ return redirect(url, [cookie({ name: loginName, value: sealed, path: paths.callback, maxAge: loginTtlMs / 1000 })]);
161
98
  }
162
99
  async function callback(request) {
163
100
  const url = new URL(request.url);
164
101
  const cookies = cookiesOf(request);
165
- const clearLogin = cookie(loginName, "", { path: paths.callback, maxAge: 0 });
102
+ const clearLogin = cookie({ name: loginName, value: "", path: paths.callback, maxAge: 0 });
166
103
  const pending = await unseal(await keys.login, cookies.get(loginName));
167
104
  const failed = (reason) => new Response(`Sign-in failed: ${reason}`, { status: 400, headers: { "set-cookie": clearLogin } });
168
105
  if (url.searchParams.has("error")) {
@@ -209,9 +146,8 @@ export function webLoginRoutes(options) {
209
146
  return callback(request);
210
147
  if (pathname === paths.logout && (request.method === "GET" || request.method === "POST")) {
211
148
  // The session cookies are SameSite=Lax, so a link from another site would otherwise sign the person out.
212
- const site = request.headers.get("sec-fetch-site");
213
- if (site === "cross-site" || site === "same-site")
214
- return new Response("Sign out from the app", { status: 403 });
149
+ if (!originAllowsRequest(request, appUrl.origin))
150
+ return new Response(crossSiteSignOutMessage, { status: 403 });
215
151
  return logout(request);
216
152
  }
217
153
  return undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"web-routes.js","sourceRoot":"","sources":["../src/web-routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"web-routes.js","sourceRoot":"","sources":["../src/web-routes.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manta-eu/auth",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Keycloak login for the SDK CLIs: browser and device-code login, silent token renewal, and credential storage; plus the default deployment the SDK targets",
5
5
  "homepage": "https://developers.manta.eu",
6
6
  "keywords": [
@@ -23,6 +23,14 @@
23
23
  "./web": {
24
24
  "types": "./dist/web.d.ts",
25
25
  "import": "./dist/web.js"
26
+ },
27
+ "./sealed-cookies": {
28
+ "types": "./dist/sealed-cookies.d.ts",
29
+ "import": "./dist/sealed-cookies.js"
30
+ },
31
+ "./request-origin": {
32
+ "types": "./dist/request-origin.d.ts",
33
+ "import": "./dist/request-origin.js"
26
34
  }
27
35
  },
28
36
  "author": "",
@@ -31,7 +39,7 @@
31
39
  "commander": "^15.0.0",
32
40
  "jose": "^6.2.12",
33
41
  "openid-client": "^6.8.8",
34
- "@manta-eu/lib": "0.12.0"
42
+ "@manta-eu/lib": "0.13.0"
35
43
  },
36
44
  "devDependencies": {
37
45
  "@types/node": "^24.13.3",
@@ -0,0 +1,21 @@
1
+ import { isPresent } from "@manta-eu/lib/present";
2
+
3
+ export const crossSiteSignOutMessage = "Sign out from the app";
4
+
5
+ export function isWebSocket(request: Request): boolean {
6
+ return request.headers.get("upgrade")?.toLowerCase() === "websocket";
7
+ }
8
+
9
+ export function originAllowsRequest(request: Request, ownOrigin = new URL(request.url).origin): boolean {
10
+ const fetchSite = request.headers.get("sec-fetch-site");
11
+
12
+ if (fetchSite === "same-origin") return true;
13
+
14
+ if (fetchSite === "cross-site" || fetchSite === "same-site") return false;
15
+
16
+ const origin = request.headers.get("origin");
17
+
18
+ if (isPresent(origin)) return origin === ownOrigin;
19
+
20
+ return !isWebSocket(request) && (request.method === "GET" || request.method === "HEAD");
21
+ }
@@ -0,0 +1,145 @@
1
+ import { isPresent } from "@manta-eu/lib/present";
2
+ import { EncryptJWT, jwtDecrypt, type JWTPayload } from "jose";
3
+
4
+ export type SealingKey = Awaited<ReturnType<typeof crypto.subtle.deriveKey>>;
5
+
6
+ const chunkSize = 3800;
7
+ const maxChunks = 10;
8
+
9
+ const encoded = (text: string) => new TextEncoder().encode(text);
10
+
11
+ export async function purposeKey(secret: string, purpose: string): Promise<SealingKey> {
12
+ const material = await crypto.subtle.importKey("raw", encoded(secret), "HKDF", false, ["deriveKey"]);
13
+ const derivation = { name: "HKDF", hash: "SHA-256", salt: encoded("manta-web-login"), info: encoded(purpose) };
14
+
15
+ return crypto.subtle.deriveKey(derivation, material, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
16
+ }
17
+
18
+ export type SealArgs = { key: SealingKey; value: JWTPayload; ttlMs?: number };
19
+
20
+ export async function seal(args: SealArgs): Promise<string> {
21
+ const { key, value, ttlMs } = args;
22
+
23
+ const jwt = new EncryptJWT({ ...value }).setProtectedHeader({ alg: "dir", enc: "A256GCM" });
24
+
25
+ if (ttlMs !== undefined) jwt.setExpirationTime(new Date(Date.now() + ttlMs));
26
+
27
+ return jwt.encrypt(key);
28
+ }
29
+
30
+ export async function unseal(key: SealingKey, value: string | undefined): Promise<JWTPayload | undefined> {
31
+ if (!isPresent(value)) return undefined;
32
+
33
+ try {
34
+ const { payload } = await jwtDecrypt(value, key);
35
+
36
+ return payload;
37
+ } catch {
38
+ return undefined;
39
+ }
40
+ }
41
+
42
+ export function cookiesOf(request: Request): Map<string, string> {
43
+ const pairs = (request.headers.get("cookie") ?? "").split(";").flatMap((part) => {
44
+ const separator = part.indexOf("=");
45
+
46
+ return separator > 0 ? [[part.slice(0, separator).trim(), part.slice(separator + 1).trim()] as const] : [];
47
+ });
48
+
49
+ return new Map(pairs);
50
+ }
51
+
52
+ export type ReturnToArgs = { raw: string | null; fallback: string; appUrl: URL };
53
+
54
+ export function safeReturnTo(args: ReturnToArgs): string {
55
+ const { raw, fallback, appUrl } = args;
56
+
57
+ if (!raw?.startsWith("/") || /[\p{Cc}\\]/u.test(raw)) return fallback;
58
+
59
+ const { origin, pathname, search, hash } = new URL(raw, appUrl);
60
+
61
+ if (origin !== appUrl.origin || pathname.startsWith("//")) return fallback;
62
+
63
+ return `${pathname}${search}${hash}`;
64
+ }
65
+
66
+ function cookieChunks(value: string): string[] {
67
+ const count = Math.ceil(value.length / chunkSize);
68
+
69
+ if (count > maxChunks) throw new Error("The session is too large to keep in cookies");
70
+
71
+ return Array.from({ length: count }, (_, index) => value.slice(index * chunkSize, (index + 1) * chunkSize));
72
+ }
73
+
74
+ function joinedChunks(cookies: Map<string, string>, name: string): string {
75
+ const names = Array.from({ length: maxChunks }, (_, index) => `${name}.${index}`);
76
+ const firstMissing = names.findIndex((chunk) => !cookies.has(chunk));
77
+
78
+ return names
79
+ .slice(0, firstMissing === -1 ? undefined : firstMissing)
80
+ .map((chunk) => cookies.get(chunk))
81
+ .join("");
82
+ }
83
+
84
+ type ChunkNamesArgs = { cookies: Map<string, string>; name: string; first: number };
85
+
86
+ function chunkNamesFrom(args: ChunkNamesArgs): string[] {
87
+ const { cookies, name, first } = args;
88
+
89
+ const prefix = `${name}.`;
90
+
91
+ return [...cookies.keys()].filter(
92
+ (cookie) => cookie.startsWith(prefix) && Number(cookie.slice(prefix.length)) >= first,
93
+ );
94
+ }
95
+
96
+ export const sessionMaxAgeSeconds = 30 * 24 * 60 * 60;
97
+
98
+ export type CookieArgs = { name: string; value: string; path?: string; maxAge?: number };
99
+
100
+ export type SetCookie = (cookie: CookieArgs) => string;
101
+
102
+ export function httpOnlyCookie(secure: boolean): SetCookie {
103
+ return (cookie) => {
104
+ const { name, value, path = "/", maxAge } = cookie;
105
+
106
+ return [
107
+ `${name}=${value}`,
108
+ `Path=${path}`,
109
+ "HttpOnly",
110
+ "SameSite=Lax",
111
+ ...(secure ? ["Secure"] : []),
112
+ ...(maxAge === undefined ? [] : [`Max-Age=${maxAge}`]),
113
+ ].join("; ");
114
+ };
115
+ }
116
+
117
+ export type ChunkCookiesArgs = { cookies: Map<string, string>; name: string; setCookie: SetCookie };
118
+
119
+ export type SealedChunkCookiesArgs = ChunkCookiesArgs & { key: SealingKey; value: JWTPayload; maxAge?: number };
120
+
121
+ export async function sealedChunkCookies(args: SealedChunkCookiesArgs): Promise<string[]> {
122
+ const { cookies, name, setCookie, key, value, maxAge } = args;
123
+
124
+ const chunks = cookieChunks(await seal({ key, value, ttlMs: maxAge === undefined ? undefined : maxAge * 1000 }));
125
+ const stale = chunkNamesFrom({ cookies, name, first: chunks.length });
126
+
127
+ return [
128
+ ...chunks.map((chunk, index) => setCookie({ name: `${name}.${index}`, value: chunk, maxAge })),
129
+ ...stale.map((chunk) => setCookie({ name: chunk, value: "", maxAge: 0 })),
130
+ ];
131
+ }
132
+
133
+ export function expiredChunkCookies(args: ChunkCookiesArgs): string[] {
134
+ const { cookies, name, setCookie } = args;
135
+
136
+ return chunkNamesFrom({ cookies, name, first: 0 }).map((chunk) => setCookie({ name: chunk, value: "", maxAge: 0 }));
137
+ }
138
+
139
+ export type UnsealedChunksArgs = Pick<ChunkCookiesArgs, "cookies" | "name"> & { key: SealingKey };
140
+
141
+ export async function unsealedChunks(args: UnsealedChunksArgs): Promise<JWTPayload | undefined> {
142
+ const { cookies, name, key } = args;
143
+
144
+ return unseal(key, joinedChunks(cookies, name));
145
+ }
package/src/web-routes.ts CHANGED
@@ -1,6 +1,18 @@
1
- import { EncryptJWT, jwtDecrypt, type JWTPayload } from "jose";
2
1
  import type { FetchLike } from "./http.js";
3
2
  import type { OidcClient } from "./oidc.js";
3
+ import { crossSiteSignOutMessage, originAllowsRequest } from "./request-origin.js";
4
+ import {
5
+ cookiesOf,
6
+ expiredChunkCookies,
7
+ purposeKey,
8
+ safeReturnTo,
9
+ seal,
10
+ sealedChunkCookies,
11
+ sessionMaxAgeSeconds,
12
+ unseal,
13
+ unsealedChunks,
14
+ httpOnlyCookie,
15
+ } from "./sealed-cookies.js";
4
16
  import { accessTokenExpiresAt } from "./token.js";
5
17
  import {
6
18
  endWebLogin,
@@ -51,78 +63,26 @@ type PendingLogin = { state: string; verifier: string; returnTo: string };
51
63
 
52
64
  type SharedRefresh = { renewed: Promise<WebTokens | undefined>; settledAt?: number; nextRefreshToken?: string };
53
65
 
54
- type SealingKey = Awaited<ReturnType<typeof crypto.subtle.deriveKey>>;
55
-
56
66
  const loginTtlMs = 10 * 60 * 1000;
57
- const offlineCookieMaxAgeSeconds = 30 * 24 * 60 * 60;
58
- const chunkSize = 3800;
59
- const maxChunks = 10;
60
67
  const refreshReuseMs = 10_000;
61
68
 
62
- // One key per purpose, so a login cookie can never be replayed as a session cookie or the other way round.
63
- async function purposeKey(secret: string, purpose: string): Promise<SealingKey> {
64
- const material = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), "HKDF", false, ["deriveKey"]);
65
- return crypto.subtle.deriveKey(
66
- {
67
- name: "HKDF",
68
- hash: "SHA-256",
69
- salt: new TextEncoder().encode("manta-web-login"),
70
- info: new TextEncoder().encode(purpose),
71
- },
72
- material,
73
- { name: "AES-GCM", length: 256 },
74
- false,
75
- ["encrypt", "decrypt"],
76
- );
77
- }
78
-
79
- async function seal(key: SealingKey, value: StoredSession | PendingLogin, ttlMs?: number): Promise<string> {
80
- const jwt = new EncryptJWT({ ...value }).setProtectedHeader({ alg: "dir", enc: "A256GCM" });
81
- if (ttlMs !== undefined) jwt.setExpirationTime(new Date(Date.now() + ttlMs));
82
- return jwt.encrypt(key);
83
- }
84
-
85
- async function unseal(key: SealingKey, value: string | undefined): Promise<JWTPayload | undefined> {
86
- if (!value) return undefined;
87
- try {
88
- return (await jwtDecrypt(value, key)).payload;
89
- } catch {
90
- return undefined;
91
- }
92
- }
93
-
94
- function cookiesOf(request: Request): Map<string, string> {
95
- const cookies = new Map<string, string>();
96
- for (const part of (request.headers.get("cookie") ?? "").split(";")) {
97
- const separator = part.indexOf("=");
98
- if (separator > 0) cookies.set(part.slice(0, separator).trim(), part.slice(separator + 1).trim());
99
- }
100
- return cookies;
101
- }
102
-
103
- // Only a path on the app itself, so `/login?returnTo=` can never send people to another site.
104
- // A browser drops tabs and newlines from a Location, so `/\t/evil.example` would land on `//evil.example`.
105
- function safeReturnTo(raw: string | null, fallback: string, appUrl: URL): string {
106
- if (!raw?.startsWith("/") || /[\p{Cc}\\]/u.test(raw)) return fallback;
107
- const resolved = new URL(raw, appUrl);
108
- // Resolving drops dot segments, so `/.//evil.example` comes out as the protocol-relative `//evil.example`.
109
- if (resolved.origin !== appUrl.origin || resolved.pathname.startsWith("//")) return fallback;
110
- return `${resolved.pathname}${resolved.search}${resolved.hash}`;
111
- }
112
-
113
69
  function isStoredSession(value: unknown): value is StoredSession {
114
70
  return typeof value === "object" && value !== null && typeof (value as StoredSession).accessToken === "string";
115
71
  }
116
72
 
117
73
  function isPendingLogin(value: unknown): value is PendingLogin {
118
74
  if (typeof value !== "object" || value === null) return false;
75
+
119
76
  const login = value as PendingLogin;
77
+
120
78
  return typeof login.state === "string" && typeof login.verifier === "string" && typeof login.returnTo === "string";
121
79
  }
122
80
 
123
81
  function redirect(location: string, setCookies: string[] = []): Response {
124
82
  const headers = new Headers({ location, "cache-control": "no-store" });
83
+
125
84
  for (const cookie of setCookies) headers.append("set-cookie", cookie);
85
+
126
86
  return new Response(null, { status: 302, headers });
127
87
  }
128
88
 
@@ -131,7 +91,7 @@ export function webLoginRoutes(options: WebLoginRoutesOptions): WebLoginRoutes {
131
91
  if (options.sessionSecret.length < 32) throw new Error("sessionSecret must be at least 32 characters");
132
92
 
133
93
  const appUrl = new URL(options.appUrl);
134
- const secure = appUrl.protocol === "https:";
94
+ const cookie = httpOnlyCookie(appUrl.protocol === "https:");
135
95
  const paths = { login: "/login", callback: "/callback", logout: "/logout", ...options.paths };
136
96
  const afterLogin = options.afterLogin ?? "/";
137
97
  const afterLogout = options.afterLogout ?? "/";
@@ -143,17 +103,22 @@ export function webLoginRoutes(options: WebLoginRoutesOptions): WebLoginRoutes {
143
103
  login: purposeKey(options.sessionSecret, "login"),
144
104
  };
145
105
  const { client, fetchImpl } = options;
106
+
146
107
  // Keycloak takes a refresh token at most twice, so requests racing one expired session share one refresh,
147
108
  // and so do requests that arrive with the old cookie shortly after it.
148
109
  const refreshes = new Map<string, SharedRefresh>();
149
110
 
150
111
  function refreshOnce(refreshToken: string): Promise<WebTokens | undefined> {
151
112
  const now = Date.now();
113
+
152
114
  for (const [token, { settledAt }] of refreshes) {
153
115
  if (settledAt !== undefined && now - settledAt > refreshReuseMs) refreshes.delete(token);
154
116
  }
117
+
155
118
  const known = refreshes.get(refreshToken);
119
+
156
120
  if (known) return known.renewed;
121
+
157
122
  const entry: SharedRefresh = { renewed: refreshWebLogin(client, refreshToken, fetchImpl) };
158
123
  refreshes.set(refreshToken, entry);
159
124
  void entry.renewed.then(
@@ -163,6 +128,7 @@ export function webLoginRoutes(options: WebLoginRoutesOptions): WebLoginRoutes {
163
128
  },
164
129
  () => refreshes.delete(refreshToken),
165
130
  );
131
+
166
132
  return entry.renewed;
167
133
  }
168
134
 
@@ -177,59 +143,48 @@ export function webLoginRoutes(options: WebLoginRoutesOptions): WebLoginRoutes {
177
143
  }
178
144
  }
179
145
 
180
- function cookie(name: string, value: string, extra: { path?: string; maxAge?: number } = {}): string {
181
- const attributes = [`${name}=${value}`, `Path=${extra.path ?? "/"}`, "HttpOnly", "SameSite=Lax"];
182
- if (secure) attributes.push("Secure");
183
- if (extra.maxAge !== undefined) attributes.push(`Max-Age=${extra.maxAge}`);
184
- return attributes.join("; ");
185
- }
186
-
187
146
  function clearedSession(cookies: Map<string, string>): string[] {
188
- return [...cookies.keys()]
189
- .filter((name) => name.startsWith(`${sessionName}.`))
190
- .map((name) => cookie(name, "", { maxAge: 0 }));
147
+ return expiredChunkCookies({ cookies, name: sessionName, setCookie: cookie });
191
148
  }
192
149
 
193
150
  async function sessionCookies(stored: StoredSession, cookies: Map<string, string>): Promise<string[]> {
194
- const value = await seal(await keys.session, stored);
195
- const chunks = Math.ceil(value.length / chunkSize);
196
- if (chunks > maxChunks) throw new Error("The session is too large to keep in cookies");
197
- const maxAge = options.offlineAccess === true ? offlineCookieMaxAgeSeconds : undefined;
198
- const written = Array.from({ length: chunks }, (_, index) =>
199
- cookie(`${sessionName}.${index}`, value.slice(index * chunkSize, (index + 1) * chunkSize), {
200
- ...(maxAge === undefined ? {} : { maxAge }),
201
- }),
202
- );
203
- const stale = [...cookies.keys()]
204
- .filter((name) => name.startsWith(`${sessionName}.`) && Number(name.slice(sessionName.length + 1)) >= chunks)
205
- .map((name) => cookie(name, "", { maxAge: 0 }));
206
- return [...written, ...stale];
151
+ const maxAge = options.offlineAccess === true ? sessionMaxAgeSeconds : undefined;
152
+
153
+ return sealedChunkCookies({
154
+ cookies,
155
+ name: sessionName,
156
+ setCookie: cookie,
157
+ key: await keys.session,
158
+ value: stored,
159
+ maxAge,
160
+ });
207
161
  }
208
162
 
209
163
  async function storedSession(cookies: Map<string, string>): Promise<StoredSession | undefined> {
210
- let value = "";
211
- for (let index = 0; cookies.has(`${sessionName}.${index}`); index += 1) {
212
- value += cookies.get(`${sessionName}.${index}`);
213
- }
214
- const stored = await unseal(await keys.session, value);
164
+ const stored = await unsealedChunks({ cookies, name: sessionName, key: await keys.session });
165
+
215
166
  return isStoredSession(stored) ? stored : undefined;
216
167
  }
217
168
 
218
169
  async function login(request: Request): Promise<Response> {
219
- const returnTo = safeReturnTo(new URL(request.url).searchParams.get("returnTo"), afterLogin, appUrl);
170
+ const raw = new URL(request.url).searchParams.get("returnTo");
171
+ const returnTo = safeReturnTo({ raw, fallback: afterLogin, appUrl });
220
172
  const { url, state, verifier } = await startWebLogin(client, {
221
173
  redirectUri,
222
174
  ...(options.offlineAccess === true ? { offlineAccess: true } : {}),
223
175
  ...(options.idpHint ? { idpHint: options.idpHint } : {}),
224
176
  });
225
- const sealed = await seal(await keys.login, { state, verifier, returnTo } satisfies PendingLogin, loginTtlMs);
226
- return redirect(url, [cookie(loginName, sealed, { path: paths.callback, maxAge: loginTtlMs / 1000 })]);
177
+
178
+ const value = { state, verifier, returnTo } satisfies PendingLogin;
179
+ const sealed = await seal({ key: await keys.login, value, ttlMs: loginTtlMs });
180
+
181
+ return redirect(url, [cookie({ name: loginName, value: sealed, path: paths.callback, maxAge: loginTtlMs / 1000 })]);
227
182
  }
228
183
 
229
184
  async function callback(request: Request): Promise<Response> {
230
185
  const url = new URL(request.url);
231
186
  const cookies = cookiesOf(request);
232
- const clearLogin = cookie(loginName, "", { path: paths.callback, maxAge: 0 });
187
+ const clearLogin = cookie({ name: loginName, value: "", path: paths.callback, maxAge: 0 });
233
188
  const pending = await unseal(await keys.login, cookies.get(loginName));
234
189
  const failed = (reason: string) =>
235
190
  new Response(`Sign-in failed: ${reason}`, { status: 400, headers: { "set-cookie": clearLogin } });
@@ -237,9 +192,12 @@ export function webLoginRoutes(options: WebLoginRoutesOptions): WebLoginRoutes {
237
192
  if (url.searchParams.has("error")) {
238
193
  return failed(url.searchParams.get("error_description") ?? url.searchParams.get("error") ?? "refused");
239
194
  }
195
+
240
196
  if (!isPendingLogin(pending)) return failed("the sign-in expired, start again");
241
197
  if (url.searchParams.get("state") !== pending.state) return failed("the sign-in does not match this browser");
198
+
242
199
  const code = url.searchParams.get("code");
200
+
243
201
  if (!code) return failed("no code");
244
202
 
245
203
  const tokens = await finishWebLogin(
@@ -252,16 +210,19 @@ export function webLoginRoutes(options: WebLoginRoutesOptions): WebLoginRoutes {
252
210
  ...(tokens.refreshToken ? { refreshToken: tokens.refreshToken } : {}),
253
211
  ...(tokens.idToken ? { idToken: tokens.idToken } : {}),
254
212
  };
213
+
255
214
  return redirect(pending.returnTo, [...(await sessionCookies(stored, cookies)), clearLogin]);
256
215
  }
257
216
 
258
217
  async function logout(request: Request): Promise<Response> {
259
218
  const cookies = cookiesOf(request);
260
219
  const stored = await storedSession(cookies);
220
+
261
221
  if (stored?.refreshToken) {
262
222
  forgetRefreshes(stored.refreshToken);
263
223
  await endWebLogin(client, stored.refreshToken, fetchImpl);
264
224
  }
225
+
265
226
  // Keycloak matches this exactly against the app's redirect URLs and home URL, which has no trailing slash.
266
227
  const landing = afterLogout === "/" ? options.appUrl.replace(/\/+$/, "") : new URL(afterLogout, appUrl).toString();
267
228
  const target =
@@ -271,30 +232,35 @@ export function webLoginRoutes(options: WebLoginRoutesOptions): WebLoginRoutes {
271
232
  ...(stored?.idToken ? { idToken: stored.idToken } : {}),
272
233
  postLogoutRedirectUri: landing,
273
234
  });
235
+
274
236
  return redirect(target, clearedSession(cookies));
275
237
  }
276
238
 
277
239
  return {
278
240
  async handle(request) {
279
241
  const { pathname } = new URL(request.url);
242
+
280
243
  if (pathname === paths.login && request.method === "GET") return login(request);
281
244
  if (pathname === paths.callback && request.method === "GET") return callback(request);
245
+
282
246
  if (pathname === paths.logout && (request.method === "GET" || request.method === "POST")) {
283
247
  // The session cookies are SameSite=Lax, so a link from another site would otherwise sign the person out.
284
- const site = request.headers.get("sec-fetch-site");
285
- if (site === "cross-site" || site === "same-site")
286
- return new Response("Sign out from the app", { status: 403 });
248
+ if (!originAllowsRequest(request, appUrl.origin)) return new Response(crossSiteSignOutMessage, { status: 403 });
249
+
287
250
  return logout(request);
288
251
  }
252
+
289
253
  return undefined;
290
254
  },
291
255
 
292
256
  async session(request) {
293
257
  const cookies = cookiesOf(request);
294
258
  const stored = await storedSession(cookies);
259
+
295
260
  if (!stored) return { accessToken: undefined, setCookies: [] };
296
261
 
297
262
  const now = Date.now();
263
+
298
264
  if (accessTokenExpiresAt(stored.accessToken, now) > now) {
299
265
  return {
300
266
  accessToken: stored.accessToken,
@@ -304,6 +270,7 @@ export function webLoginRoutes(options: WebLoginRoutesOptions): WebLoginRoutes {
304
270
  }
305
271
 
306
272
  const renewed = stored.refreshToken ? await refreshOnce(stored.refreshToken) : undefined;
273
+
307
274
  if (!renewed) return { accessToken: undefined, setCookies: clearedSession(cookies) };
308
275
 
309
276
  const next: StoredSession = {
@@ -311,6 +278,7 @@ export function webLoginRoutes(options: WebLoginRoutesOptions): WebLoginRoutes {
311
278
  ...(renewed.refreshToken ? { refreshToken: renewed.refreshToken } : {}),
312
279
  ...((renewed.idToken ?? stored.idToken) ? { idToken: renewed.idToken ?? stored.idToken } : {}),
313
280
  };
281
+
314
282
  return {
315
283
  accessToken: next.accessToken,
316
284
  ...(next.idToken ? { idToken: next.idToken } : {}),