@shware/security 7.1.1 → 8.0.1

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.
Files changed (132) hide show
  1. package/README.md +113 -32
  2. package/dist/auth/attributes.cjs +4 -3
  3. package/dist/auth/attributes.cjs.map +1 -1
  4. package/dist/auth/attributes.d.cts +3 -3
  5. package/dist/auth/attributes.d.cts.map +1 -1
  6. package/dist/auth/attributes.d.mts +3 -3
  7. package/dist/auth/attributes.d.mts.map +1 -1
  8. package/dist/auth/attributes.mjs +4 -3
  9. package/dist/auth/attributes.mjs.map +1 -1
  10. package/dist/auth/desktop-codes.cjs +57 -0
  11. package/dist/auth/desktop-codes.cjs.map +1 -0
  12. package/dist/auth/desktop-codes.d.cts +24 -0
  13. package/dist/auth/desktop-codes.d.cts.map +1 -0
  14. package/dist/auth/desktop-codes.d.mts +24 -0
  15. package/dist/auth/desktop-codes.d.mts.map +1 -0
  16. package/dist/auth/desktop-codes.mjs +55 -0
  17. package/dist/auth/desktop-codes.mjs.map +1 -0
  18. package/dist/auth/email-codes.cjs +77 -0
  19. package/dist/auth/email-codes.cjs.map +1 -0
  20. package/dist/auth/email-codes.d.cts +39 -0
  21. package/dist/auth/email-codes.d.cts.map +1 -0
  22. package/dist/auth/email-codes.d.mts +39 -0
  23. package/dist/auth/email-codes.d.mts.map +1 -0
  24. package/dist/auth/email-codes.mjs +75 -0
  25. package/dist/auth/email-codes.mjs.map +1 -0
  26. package/dist/auth/error.cjs +47 -0
  27. package/dist/auth/error.cjs.map +1 -0
  28. package/dist/auth/error.d.cts +88 -0
  29. package/dist/auth/error.d.cts.map +1 -0
  30. package/dist/auth/error.d.mts +88 -0
  31. package/dist/auth/error.d.mts.map +1 -0
  32. package/dist/auth/error.mjs +45 -0
  33. package/dist/auth/error.mjs.map +1 -0
  34. package/dist/auth/index.cjs +249 -384
  35. package/dist/auth/index.cjs.map +1 -1
  36. package/dist/auth/index.d.cts +66 -55
  37. package/dist/auth/index.d.cts.map +1 -1
  38. package/dist/auth/index.d.mts +66 -55
  39. package/dist/auth/index.d.mts.map +1 -1
  40. package/dist/auth/index.mjs +252 -387
  41. package/dist/auth/index.mjs.map +1 -1
  42. package/dist/auth/respond.cjs +99 -0
  43. package/dist/auth/respond.cjs.map +1 -0
  44. package/dist/auth/respond.d.cts +39 -0
  45. package/dist/auth/respond.d.cts.map +1 -0
  46. package/dist/auth/respond.d.mts +39 -0
  47. package/dist/auth/respond.d.mts.map +1 -0
  48. package/dist/auth/respond.mjs +92 -0
  49. package/dist/auth/respond.mjs.map +1 -0
  50. package/dist/auth/types.d.cts +27 -11
  51. package/dist/auth/types.d.cts.map +1 -1
  52. package/dist/auth/types.d.mts +27 -11
  53. package/dist/auth/types.d.mts.map +1 -1
  54. package/dist/index.cjs +5 -0
  55. package/dist/index.d.cts +4 -1
  56. package/dist/index.d.mts +4 -1
  57. package/dist/index.mjs +3 -1
  58. package/dist/oauth2/error.cjs +0 -7
  59. package/dist/oauth2/error.cjs.map +1 -1
  60. package/dist/oauth2/error.d.cts +0 -3
  61. package/dist/oauth2/error.d.cts.map +1 -1
  62. package/dist/oauth2/error.d.mts +0 -3
  63. package/dist/oauth2/error.d.mts.map +1 -1
  64. package/dist/oauth2/error.mjs +1 -7
  65. package/dist/oauth2/error.mjs.map +1 -1
  66. package/dist/oauth2/provider/common.cjs +17 -3
  67. package/dist/oauth2/provider/common.cjs.map +1 -1
  68. package/dist/oauth2/provider/common.d.cts.map +1 -1
  69. package/dist/oauth2/provider/common.d.mts.map +1 -1
  70. package/dist/oauth2/provider/common.mjs +17 -3
  71. package/dist/oauth2/provider/common.mjs.map +1 -1
  72. package/dist/oauth2/provider/facebook.cjs +2 -4
  73. package/dist/oauth2/provider/facebook.cjs.map +1 -1
  74. package/dist/oauth2/provider/facebook.d.cts +0 -1
  75. package/dist/oauth2/provider/facebook.d.cts.map +1 -1
  76. package/dist/oauth2/provider/facebook.d.mts +0 -1
  77. package/dist/oauth2/provider/facebook.d.mts.map +1 -1
  78. package/dist/oauth2/provider/facebook.mjs +2 -4
  79. package/dist/oauth2/provider/facebook.mjs.map +1 -1
  80. package/dist/oauth2/provider/github.cjs +1 -1
  81. package/dist/oauth2/provider/github.cjs.map +1 -1
  82. package/dist/oauth2/provider/github.d.cts +1 -1
  83. package/dist/oauth2/provider/github.d.cts.map +1 -1
  84. package/dist/oauth2/provider/github.d.mts +1 -1
  85. package/dist/oauth2/provider/github.d.mts.map +1 -1
  86. package/dist/oauth2/provider/github.mjs +1 -1
  87. package/dist/oauth2/provider/github.mjs.map +1 -1
  88. package/dist/oauth2/provider/x.cjs +6 -4
  89. package/dist/oauth2/provider/x.cjs.map +1 -1
  90. package/dist/oauth2/provider/x.d.cts +3 -2
  91. package/dist/oauth2/provider/x.d.cts.map +1 -1
  92. package/dist/oauth2/provider/x.d.mts +3 -2
  93. package/dist/oauth2/provider/x.d.mts.map +1 -1
  94. package/dist/oauth2/provider/x.mjs +6 -4
  95. package/dist/oauth2/provider/x.mjs.map +1 -1
  96. package/dist/oauth2/types.cjs.map +1 -1
  97. package/dist/oauth2/types.d.cts +5 -0
  98. package/dist/oauth2/types.d.cts.map +1 -1
  99. package/dist/oauth2/types.d.mts +5 -0
  100. package/dist/oauth2/types.d.mts.map +1 -1
  101. package/dist/oauth2/types.mjs.map +1 -1
  102. package/dist/session/common.cjs +10 -13
  103. package/dist/session/common.cjs.map +1 -1
  104. package/dist/session/common.d.cts +7 -3
  105. package/dist/session/common.d.cts.map +1 -1
  106. package/dist/session/common.d.mts +7 -3
  107. package/dist/session/common.d.mts.map +1 -1
  108. package/dist/session/common.mjs +10 -12
  109. package/dist/session/common.mjs.map +1 -1
  110. package/dist/session/db.cjs +2 -2
  111. package/dist/session/db.cjs.map +1 -1
  112. package/dist/session/db.mjs +3 -3
  113. package/dist/session/db.mjs.map +1 -1
  114. package/dist/session/redis.cjs +6 -8
  115. package/dist/session/redis.cjs.map +1 -1
  116. package/dist/session/redis.d.cts.map +1 -1
  117. package/dist/session/redis.d.mts.map +1 -1
  118. package/dist/session/redis.mjs +7 -9
  119. package/dist/session/redis.mjs.map +1 -1
  120. package/dist/session/types.d.cts +0 -12
  121. package/dist/session/types.d.cts.map +1 -1
  122. package/dist/session/types.d.mts +0 -12
  123. package/dist/session/types.d.mts.map +1 -1
  124. package/dist/utils/valid.cjs +34 -52
  125. package/dist/utils/valid.cjs.map +1 -1
  126. package/dist/utils/valid.d.cts +8 -23
  127. package/dist/utils/valid.d.cts.map +1 -1
  128. package/dist/utils/valid.d.mts +8 -23
  129. package/dist/utils/valid.d.mts.map +1 -1
  130. package/dist/utils/valid.mjs +32 -50
  131. package/dist/utils/valid.mjs.map +1 -1
  132. package/package.json +3 -3
@@ -1,20 +1,25 @@
1
- import { MAX_ATTRIBUTES_BYTES, attributesSize, bodyAttributes } from "./attributes.mjs";
2
- import { loginEmailSchema, sendEmailVerificationCodeSchema } from "../email/schema.mjs";
1
+ import { AuthenticationError } from "./error.mjs";
3
2
  import { OAuth2Error, isKnownOAuth2ErrorType } from "../oauth2/error.mjs";
3
+ import { errorResponse, invalidArgument, unauthenticated } from "./respond.mjs";
4
+ import { MAX_ATTRIBUTES_BYTES, attributesSchema, attributesSize, bodyAttributes } from "./attributes.mjs";
5
+ import { loginEmailSchema, sendEmailVerificationCodeSchema } from "../email/schema.mjs";
4
6
  import { OAuth2Client, googleOneTapSchema, oauth2RedirectQuerySchema } from "../oauth2/client.mjs";
5
7
  import { PRINCIPAL_NAME_INDEX_NAME } from "../session/common.mjs";
6
8
  import { deleteCookie, getCookie, param, query, setCookie } from "../utils/http.mjs";
7
9
  import { timing } from "../utils/timing.mjs";
8
- import { invalidArgument, resourceExhausted, valid, verifyTurnstileToken } from "../utils/valid.mjs";
9
- import { createHash, randomInt, randomUUID, timingSafeEqual } from "node:crypto";
10
+ import { parse, readForm, readJson, verifyTurnstileToken } from "../utils/valid.mjs";
11
+ import { DesktopCodes } from "./desktop-codes.mjs";
12
+ import { EmailVerificationCodes } from "./email-codes.mjs";
13
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
10
14
  import { invariant } from "@shware/utils";
15
+ import { extend, literal, object, regex, string } from "zod/mini";
11
16
  // src/auth/index.ts
12
17
  const PKCE_FORMAT = /^[A-Za-z0-9_-]{43,128}$/;
13
18
  const CODE_VERIFIER_FORMAT = /^[A-Za-z0-9._~-]{43,128}$/;
14
- const INVALID_CODE_VERIFIER = {
15
- error: "invalid_request",
16
- error_description: "code_verifier must be 43-128 characters from [A-Za-z0-9-._~]"
17
- };
19
+ /** The header a session id travels in besides the cookie, with `session.header` on. */
20
+ const SESSION_HEADER = "X-Auth-Token";
21
+ /** Seconds an OAuth2 state or nonce may be used within. */
22
+ const OAUTH2_PARAMETER_TTL = 600;
18
23
  const MAX_ERROR_DESCRIPTION_LENGTH = 256;
19
24
  /** An error code safe to put on errorUri: a registered one as is, anything else as `fallback`. */
20
25
  function safeErrorType(error, fallback) {
@@ -41,14 +46,20 @@ function authorizationAttributes(attributes) {
41
46
  return defined;
42
47
  }
43
48
  /** Sign in with Apple answers with `response_mode=form_post`; a charset parameter may follow. */
44
- const INVALID_ATTRIBUTES = {
45
- error: "invalid_request",
46
- error_description: `attributes must map strings to strings, at most ${MAX_ATTRIBUTES_BYTES} bytes`
47
- };
48
49
  function isFormPost(request) {
49
50
  const contentType = request.headers.get("content-type")?.toLowerCase();
50
51
  return request.method === "POST" && !!contentType?.startsWith("application/x-www-form-urlencoded");
51
52
  }
53
+ /** Google's own form post carries no attributes; an app posting JSON may add them. */
54
+ const oneTapBodySchema = extend(googleOneTapSchema, { attributes: attributesSchema });
55
+ const desktopAuthorizeSchema = object({
56
+ code_challenge: string().check(regex(PKCE_FORMAT)),
57
+ code_challenge_method: literal("S256")
58
+ });
59
+ const desktopExchangeSchema = object({
60
+ code: string(),
61
+ code_verifier: string().check(regex(PKCE_FORMAT))
62
+ });
52
63
  const PATH = {
53
64
  CSRF: "/csrf",
54
65
  LOGOUT: "/logout",
@@ -60,15 +71,18 @@ const PATH = {
60
71
  LOGIN_OAUTH2_NATIVE: "/login/oauth2/native/:provider",
61
72
  LOGIN_OAUTH2_ONETAP: "/login/oauth2/onetap/google",
62
73
  LOGIN_EMAIL: "/login/email",
63
- LOGIN_PHONE: "/login/phone",
64
74
  SEND_EMAIL_VERIFICATION_CODE: "/auth/sendEmailVerificationCode",
65
- SEND_PHONE_VERIFICATION_CODE: "/auth/sendPhoneVerificationCode",
66
75
  DESKTOP_AUTHORIZE: "/auth/desktop/authorize",
67
76
  DESKTOP_EXCHANGE: "/auth/desktop/exchange",
68
77
  CLEANUP_EXPIRED_SESSIONS: "/sessions/expired/cleanup"
69
78
  };
70
- /** Seconds an email verification code, and its attempt counter, stay valid. */
71
- const EMAIL_VERIFICATION_CODE_TTL = 600;
79
+ /**
80
+ * The login routes and session helpers. Every route is one straight line: read and validate the
81
+ * request, check the credential, call the host's `onAuthorized`, sign in. Failures are thrown and
82
+ * answered in one place per kind of route: a JSON route runs under `guard`, which maps a
83
+ * `RequestError`, an `AuthenticationError` or an `OAuth2Error` to its google.rpc body (see
84
+ * `respond.ts`); the redirect callback maps the same errors to `errorUri` parameters (`redirect`).
85
+ */
72
86
  var Auth = class {
73
87
  timing;
74
88
  cookieName;
@@ -76,21 +90,27 @@ var Auth = class {
76
90
  kv;
77
91
  repository;
78
92
  turnstileSecretKey;
79
- maxVerificationAttempts;
80
- verificationResendCooldown;
93
+ emailCodes;
94
+ desktopCodes;
81
95
  oauth2Client;
82
96
  onetapRequireNonce;
83
97
  touchInterval;
98
+ sessionHeader;
84
99
  ATTR_OAUTH2_AUTHORIZATION_REQUEST = "oauth2AuthorizationRequest";
85
100
  constructor({ sessionRepository, kvRepository, turnstileSecretKey, verificationCode, oauth2, cookie, session, timing }) {
86
101
  this.timing = timing ?? false;
87
102
  this.kv = kvRepository;
88
103
  this.repository = sessionRepository;
89
104
  this.turnstileSecretKey = turnstileSecretKey;
90
- this.maxVerificationAttempts = verificationCode?.maxAttempts ?? 5;
91
- this.verificationResendCooldown = verificationCode?.resendCooldown ?? 60;
92
- invariant(this.maxVerificationAttempts >= 1, "verificationCode.maxAttempts must be at least 1");
93
- invariant(this.verificationResendCooldown >= 1, "verificationCode.resendCooldown must be at least 1 second");
105
+ const maxAttempts = verificationCode?.maxAttempts ?? 5;
106
+ const resendCooldown = verificationCode?.resendCooldown ?? 60;
107
+ invariant(maxAttempts >= 1, "verificationCode.maxAttempts must be at least 1");
108
+ invariant(resendCooldown >= 1, "verificationCode.resendCooldown must be at least 1 second");
109
+ this.emailCodes = new EmailVerificationCodes(kvRepository, {
110
+ maxAttempts,
111
+ resendCooldown
112
+ });
113
+ this.desktopCodes = new DesktopCodes(kvRepository);
94
114
  const { name, ...cookieOptions } = cookie ?? {};
95
115
  this.cookieName = name ?? "SESSION";
96
116
  this.cookieOptions = {
@@ -103,14 +123,76 @@ var Auth = class {
103
123
  this.oauth2Client = oauth2?.client ? new OAuth2Client(oauth2.client) : null;
104
124
  this.onetapRequireNonce = oauth2?.onetap?.requireNonce ?? true;
105
125
  this.touchInterval = (session?.touchInterval ?? 3600) * 1e3;
126
+ this.sessionHeader = session?.header ?? false;
106
127
  invariant(this.touchInterval >= 0, "session.touchInterval must not be negative");
107
128
  }
108
129
  getSessionId(request) {
109
- let sessionId;
110
- const header = request.headers.get("Authorization");
111
- if (header?.startsWith("Bearer ")) sessionId = header.replace("Bearer ", "");
112
- if (!sessionId) sessionId = getCookie(request, this.cookieName);
113
- return sessionId;
130
+ const fromCookie = getCookie(request, this.cookieName);
131
+ if (fromCookie) return fromCookie;
132
+ return this.sessionHeader && request.headers.get(SESSION_HEADER) || void 0;
133
+ }
134
+ /** The session the request carries, if it exists. */
135
+ async loadSession(request) {
136
+ const sessionId = this.getSessionId(request);
137
+ return sessionId ? this.repository.findById(sessionId) : null;
138
+ }
139
+ /** The signed-in principal of a session, if any. */
140
+ principalOf(session) {
141
+ const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);
142
+ return name ? { name: String(name) } : null;
143
+ }
144
+ /**
145
+ * A new session signed in as `principal`. With `carryOver`, what the host kept in the pre-login
146
+ * session moves to the new one; the authorization request (state, PKCE verifier, flow
147
+ * attributes) is single-use and was consumed, and a principal left there from an earlier
148
+ * sign-in is replaced. The new id is never the one the browser brought, which may have been
149
+ * planted by an attacker who started the flow (session fixation).
150
+ */
151
+ async signIn(principal, carryOver) {
152
+ const session = this.repository.createSession();
153
+ for (const attribute of carryOver?.getAttributeNames() ?? []) {
154
+ if (attribute === this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) continue;
155
+ if (attribute === "principalName") continue;
156
+ const value = carryOver?.getAttribute(attribute);
157
+ if (value !== null && value !== void 0) session.setAttribute(attribute, value);
158
+ }
159
+ session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);
160
+ await this.repository.save(session);
161
+ return session;
162
+ }
163
+ /**
164
+ * `response` carrying `session`: its cookie, renewed for the whole inactive interval, and with
165
+ * `session.header` on, the `X-Auth-Token` header a client without a cookie jar reads.
166
+ */
167
+ withSession(response, session, cookie) {
168
+ const maxAge = session.getMaxInactiveInterval();
169
+ setCookie(response, this.cookieName, session.getId(), {
170
+ ...this.cookieOptions,
171
+ ...cookie,
172
+ maxAge
173
+ });
174
+ if (this.sessionHeader) response.headers.set(SESSION_HEADER, session.getId());
175
+ return response;
176
+ }
177
+ /** Runs a JSON route: a failure it knows is its response, anything else is the host's. */
178
+ async guard(run) {
179
+ try {
180
+ return await run();
181
+ } catch (error) {
182
+ const response = errorResponse(error);
183
+ if (response) return response;
184
+ throw error;
185
+ }
186
+ }
187
+ /** The error redirect of the callback flow; `refusal`'s reason and metadata ride along. */
188
+ redirect(error, description, refusal) {
189
+ invariant(this.oauth2Client, "oauth2Client is not initialized");
190
+ const uri = new URL(this.oauth2Client.errorUri);
191
+ for (const [key, value] of Object.entries(refusal?.metadata ?? {})) if (!uri.searchParams.has(key)) uri.searchParams.set(key, value);
192
+ uri.searchParams.set("error", error);
193
+ if (description) uri.searchParams.set("error_description", description.slice(0, MAX_ERROR_DESCRIPTION_LENGTH));
194
+ if (refusal) uri.searchParams.set("reason", refusal.reason);
195
+ return Response.redirect(uri.href, 302);
114
196
  }
115
197
  csrf = async (_request) => {
116
198
  const token = randomUUID();
@@ -136,24 +218,16 @@ var Auth = class {
136
218
  return response;
137
219
  };
138
220
  logged = async (request, onLogged) => {
139
- const sessionId = this.getSessionId(request);
140
- if (!sessionId) return Response.json({ data: false });
141
- const session = await this.repository.findById(sessionId);
221
+ const session = await this.loadSession(request);
142
222
  if (!session) return Response.json({ data: false });
143
- const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);
144
223
  session.setLastAccessedTime(Date.now());
145
224
  await this.repository.save(session);
146
- if (name !== null) await onLogged?.({ name: String(name) });
147
- const response = Response.json({ data: name !== null });
148
- const maxAge = session.getMaxInactiveInterval();
149
- setCookie(response, this.cookieName, session.getId(), {
150
- ...this.cookieOptions,
151
- maxAge
152
- });
153
- return response;
225
+ const principal = this.principalOf(session);
226
+ if (principal) await onLogged?.(principal);
227
+ return this.withSession(Response.json({ data: principal !== null }), session);
154
228
  };
155
229
  createPkceParameters() {
156
- const code_verifier = randomUUID();
230
+ const code_verifier = randomBytes(32).toString("base64url");
157
231
  return {
158
232
  code_verifier,
159
233
  code_challenge: createHash("sha256").update(code_verifier).digest().toString("base64url"),
@@ -166,34 +240,29 @@ var Auth = class {
166
240
  getOauth2NonceKey(nonce) {
167
241
  return `oauth2:nonce:${nonce}`;
168
242
  }
169
- getEmailVerificationCodeKey(email) {
170
- return `email:verification:${email}`;
171
- }
172
- getEmailVerificationAttemptsKey(email) {
173
- return `email:verification-attempts:${email}`;
174
- }
175
- getEmailVerificationCooldownKey(email) {
176
- return `email:verification-cooldown:${email}`;
177
- }
178
- getPhoneVerificationCodeKey(phone) {
179
- return `phone:verification:${phone}`;
180
- }
181
- getDesktopCodeKey(code) {
182
- return `desktop:code:${code}`;
243
+ /**
244
+ * Spends a nonce `oauth2Nonce` issued for `provider`: `NONCE_MISMATCH` for one that was not,
245
+ * or was used already. An id token is replayable until it expires; the nonce it carries makes
246
+ * it sign in once.
247
+ */
248
+ async consumeNonce(nonce, provider) {
249
+ const key = this.getOauth2NonceKey(nonce);
250
+ if (await this.kv.getItem(key) !== provider) throw new AuthenticationError("NONCE_MISMATCH");
251
+ await this.kv.removeItem(key);
183
252
  }
184
253
  oauth2State = async (request) => {
185
254
  const { provider } = param(request, PATH.OAUTH2_STATE);
186
255
  const state = randomUUID();
187
- await this.kv.setItem(this.getOauth2StateKey(state), provider, 600);
256
+ await this.kv.setItem(this.getOauth2StateKey(state), provider, OAUTH2_PARAMETER_TTL);
188
257
  return Response.json({ state });
189
258
  };
190
259
  oauth2Nonce = async (request) => {
191
260
  const { provider } = param(request, PATH.OAUTH2_NONCE);
192
261
  const nonce = randomUUID();
193
- await this.kv.setItem(this.getOauth2NonceKey(nonce), provider, 600);
262
+ await this.kv.setItem(this.getOauth2NonceKey(nonce), provider, OAUTH2_PARAMETER_TTL);
194
263
  return Response.json({ nonce });
195
264
  };
196
- oauth2Authorization = async (request, options) => {
265
+ oauth2Authorization = (request, options) => this.guard(async () => {
197
266
  invariant(this.oauth2Client, "oauth2Client is not initialized");
198
267
  const attributes = authorizationAttributes(options?.attributes);
199
268
  const { mark, setTiming } = timing({ enabled: this.timing });
@@ -205,8 +274,11 @@ var Auth = class {
205
274
  state,
206
275
  pkce
207
276
  });
208
- const sessionId = this.getSessionId(request);
209
- const session = sessionId ? await this.repository.findById(sessionId) ?? this.repository.createSession() : this.repository.createSession();
277
+ let session = await this.loadSession(request);
278
+ if (!session) {
279
+ session = this.repository.createSession();
280
+ session.setMaxInactiveInterval(OAUTH2_PARAMETER_TTL);
281
+ }
210
282
  mark("fetch_session");
211
283
  const authorizationRequest = {
212
284
  state,
@@ -215,56 +287,38 @@ var Auth = class {
215
287
  additionalParameters: pkce,
216
288
  ...attributes ? { attributes } : {}
217
289
  };
218
- const value = JSON.stringify(authorizationRequest);
219
- session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, value);
290
+ session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, JSON.stringify(authorizationRequest));
220
291
  await this.repository.save(session);
221
292
  mark("save_session");
222
293
  const response = new Response(null, {
223
294
  status: 302,
224
295
  headers: { location: uri.href }
225
296
  });
226
- const maxAge = session.getMaxInactiveInterval();
227
- setCookie(response, this.cookieName, session.getId(), {
228
- ...this.cookieOptions,
229
- maxAge
230
- });
231
297
  setTiming(response);
232
- return response;
233
- };
234
- redirect = (error, description) => {
235
- invariant(this.oauth2Client, "oauth2Client is not initialized");
236
- const uri = new URL(this.oauth2Client.errorUri);
237
- uri.searchParams.set("error", error);
238
- if (description) uri.searchParams.set("error_description", description.slice(0, MAX_ERROR_DESCRIPTION_LENGTH));
239
- return Response.redirect(uri.href, 302);
240
- };
298
+ return this.withSession(response, session);
299
+ });
241
300
  loginOAuth2Code = async (request, handler) => {
242
301
  invariant(this.oauth2Client, "oauth2Client is not initialized");
243
302
  const { mark, setTiming } = timing({ enabled: this.timing });
244
- const sessionId = this.getSessionId(request);
245
- const session = sessionId ? await this.repository.findById(sessionId) : null;
303
+ const session = await this.loadSession(request);
246
304
  if (!session) return this.redirect("invalid_request", "session not found");
247
305
  mark("load_session");
248
306
  const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);
249
307
  if (!json) return this.redirect("invalid_request", "authorization request not found");
250
308
  const cached = JSON.parse(json);
251
309
  const { provider } = param(request, PATH.LOGIN_OAUTH2_CODE);
252
- let data;
253
- if (isFormPost(request)) {
254
- const params = new URLSearchParams(await request.text());
255
- data = Object.fromEntries(params.entries());
256
- } else data = query(request);
310
+ const data = isFormPost(request) ? Object.fromEntries(new URLSearchParams(await request.text())) : query(request);
257
311
  const parsed = oauth2RedirectQuerySchema.safeParse(data);
258
312
  if (!parsed.success) return this.redirect("invalid_request", "invalid redirect query");
259
313
  if (parsed.data.error) {
260
314
  if (parsed.data.state && parsed.data.state !== cached.state) return this.redirect("invalid_request", "redirect state mismatch");
261
315
  return this.redirect(safeErrorType(parsed.data.error, "server_error"), parsed.data.error_description);
262
316
  }
263
- if (!parsed.data.code || !parsed.data.state) return this.redirect("invalid_request", "invalid redirect query");
264
- if (parsed.data.state !== cached.state) return this.redirect("invalid_request", "redirect state mismatch");
317
+ const { code, state, user } = parsed.data;
318
+ if (!code || !state) return this.redirect("invalid_request", "invalid redirect query");
319
+ if (state !== cached.state) return this.redirect("invalid_request", "redirect state mismatch");
265
320
  if (provider !== cached.provider) return this.redirect("invalid_request", "redirect provider mismatch");
266
321
  mark("validate_session");
267
- const { code, user } = parsed.data;
268
322
  let signedIn;
269
323
  try {
270
324
  const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({
@@ -287,8 +341,7 @@ var Auth = class {
287
341
  attributes: cached.attributes ?? {}
288
342
  });
289
343
  mark("save_principal");
290
- signedIn = this.signedInSession(session, principal.name);
291
- await this.repository.save(signedIn);
344
+ signedIn = await this.signIn(principal, session);
292
345
  await this.repository.deleteById(session.getId());
293
346
  mark("update_session");
294
347
  } catch (error) {
@@ -297,6 +350,10 @@ var Auth = class {
297
350
  const type = CALLBACK_ERROR_TYPES.has(error.error) ? error.error : "server_error";
298
351
  return this.redirect(type);
299
352
  }
353
+ if (error instanceof AuthenticationError) {
354
+ console.warn("OAuth2 sign-in refused", error.reason, error.metadata);
355
+ return this.redirect("access_denied", void 0, error);
356
+ }
300
357
  console.error("OAuth2 callback failed", error);
301
358
  return this.redirect("server_error");
302
359
  }
@@ -307,328 +364,157 @@ var Auth = class {
307
364
  status: 302,
308
365
  headers: { location: uri.href }
309
366
  });
310
- const maxAge = signedIn.getMaxInactiveInterval();
311
- setCookie(response, this.cookieName, signedIn.getId(), {
312
- ...this.cookieOptions,
313
- maxAge
314
- });
315
367
  setTiming(response);
316
- return response;
368
+ return this.withSession(response, signedIn);
317
369
  };
318
- /**
319
- * A fresh session for `name`, carrying over what the host kept in the pre-login one. The
320
- * authorization request (state, PKCE verifier, flow attributes) is single-use and was consumed
321
- * by this callback, and a principal left there from an earlier sign-in is replaced.
322
- */
323
- signedInSession(previous, name) {
324
- const session = this.repository.createSession();
325
- for (const attribute of previous.getAttributeNames()) {
326
- if (attribute === this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) continue;
327
- if (attribute === "org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME") continue;
328
- const value = previous.getAttribute(attribute);
329
- if (value !== null) session.setAttribute(attribute, value);
330
- }
331
- session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, name);
332
- return session;
370
+ /** The shared tail of the JSON login routes: the host's handler, then a signed-in session. */
371
+ async authorize(request, handler, authorization, setTiming) {
372
+ const principal = await handler(request, authorization);
373
+ const session = await this.signIn(principal);
374
+ const response = Response.json(principal);
375
+ setTiming(response);
376
+ return this.withSession(response, session);
333
377
  }
334
- loginOAuth2Native = async (request, handler) => {
378
+ loginOAuth2Native = (request, handler) => this.guard(async () => {
335
379
  invariant(this.oauth2Client, "oauth2Client is not initialized");
336
380
  const { mark, setTiming } = timing({ enabled: this.timing });
337
381
  const { provider } = param(request, PATH.LOGIN_OAUTH2_NATIVE);
338
- const body = await request.json();
382
+ const body = await readJson(request);
339
383
  const attributes = bodyAttributes(body);
340
- if (!attributes) return Response.json(INVALID_ATTRIBUTES, { status: 400 });
341
384
  const { attributes: _, ...credentials } = body;
342
385
  const { code_verifier } = credentials;
343
- if (code_verifier !== void 0 && (typeof code_verifier !== "string" || !CODE_VERIFIER_FORMAT.test(code_verifier))) return Response.json(INVALID_CODE_VERIFIER, { status: 400 });
386
+ if (code_verifier !== void 0 && (typeof code_verifier !== "string" || !CODE_VERIFIER_FORMAT.test(code_verifier))) throw invalidArgument("INVALID_CODE_VERIFIER", "code_verifier must be 43-128 characters from [A-Za-z0-9-._~]");
344
387
  const { clientId, userInfo, token } = await this.oauth2Client.loginOAuth2Native({
345
388
  provider,
346
389
  credentials
347
390
  });
348
391
  mark("login_oauth2_native");
349
392
  const { nonce } = userInfo.data;
350
- if (nonce) {
351
- if (await this.kv.getItem(this.getOauth2NonceKey(nonce)) !== provider) return Response.json({
352
- error: "invalid_request",
353
- error_description: "nonce mismatch"
354
- }, { status: 400 });
355
- await this.kv.removeItem(this.getOauth2NonceKey(nonce));
393
+ if (typeof nonce === "string" && nonce) {
394
+ await this.consumeNonce(nonce, provider);
356
395
  mark("check_nonce");
357
396
  }
358
- const principal = await handler(request, {
397
+ return this.authorize(request, handler, {
359
398
  provider,
360
399
  clientId,
361
400
  userInfo,
362
401
  token,
363
402
  attributes
364
- });
365
- mark("save_principal");
366
- const session = this.repository.createSession();
367
- session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);
368
- await this.repository.save(session);
369
- const response = new Response(JSON.stringify(principal), {
370
- status: 200,
371
- headers: { "Content-Type": "application/json" }
372
- });
373
- const maxAge = session.getMaxInactiveInterval();
374
- setCookie(response, this.cookieName, session.getId(), {
375
- ...this.cookieOptions,
376
- maxAge
377
- });
378
- setTiming(response);
379
- return response;
380
- };
381
- loginOAuth2Onetap = async (request, handler, provider = "google") => {
403
+ }, setTiming);
404
+ });
405
+ loginOAuth2Onetap = (request, handler, provider = "google") => this.guard(async () => {
382
406
  invariant(this.oauth2Client, "oauth2Client is not initialized");
383
407
  const { mark, setTiming } = timing({ enabled: this.timing });
384
- let body;
385
- let attributes = {};
386
- if (request.headers.get("Content-Type")?.includes("application/x-www-form-urlencoded")) {
387
- const text = await request.text();
388
- body = Object.fromEntries(new URLSearchParams(text));
389
- } else if (request.headers.get("Content-Type")?.includes("application/json")) {
390
- const json = await request.json();
391
- attributes = bodyAttributes(json);
392
- body = json;
393
- } else return Response.json({
394
- error: "invalid_request",
395
- error_description: "invalid content type"
396
- }, { status: 400 });
397
- if (!attributes) return Response.json(INVALID_ATTRIBUTES, { status: 400 });
398
- const parsed = googleOneTapSchema.parse(body);
399
- let principal;
408
+ const body = (request.headers.get("content-type") ?? "").includes("application/json") ? await readJson(request) : await readForm(request);
409
+ const { attributes = {}, ...parsed } = parse(oneTapBodySchema, body);
410
+ let clientId;
411
+ let token;
412
+ let userInfo;
400
413
  if (parsed.credential) {
401
- const { clientId, token, userInfo } = await this.oauth2Client.getTokenInfo({
414
+ ({clientId, token, userInfo} = await this.oauth2Client.getTokenInfo({
402
415
  provider,
403
416
  credential: parsed.credential
404
- });
417
+ }));
405
418
  mark("verify_credential");
406
419
  const { nonce } = userInfo.data;
407
420
  if (typeof nonce === "string" && nonce) {
408
- if (await this.kv.getItem(this.getOauth2NonceKey(nonce)) !== provider) return Response.json({
409
- error: "invalid_request",
410
- error_description: "nonce mismatch"
411
- }, { status: 401 });
412
- await this.kv.removeItem(this.getOauth2NonceKey(nonce));
421
+ await this.consumeNonce(nonce, provider);
413
422
  mark("check_nonce");
414
- } else if (this.onetapRequireNonce) return Response.json({
415
- error: "invalid_request",
416
- error_description: "nonce missing"
417
- }, { status: 401 });
418
- principal = await handler(request, {
419
- provider,
420
- clientId,
421
- userInfo,
422
- token,
423
- attributes
424
- });
423
+ } else if (this.onetapRequireNonce) throw new AuthenticationError("NONCE_MISSING");
425
424
  } else if (parsed.code) {
426
- const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({
425
+ ({clientId, token} = await this.oauth2Client.exchangeAuthorizationCode({
427
426
  provider,
428
427
  code: parsed.code
429
- });
430
- principal = await handler(request, {
428
+ }));
429
+ userInfo = await this.oauth2Client.getUserInfo({
431
430
  provider,
432
431
  clientId,
433
- userInfo: await this.oauth2Client.getUserInfo({
434
- provider,
435
- clientId,
436
- token
437
- }),
438
- token,
439
- attributes
432
+ token
440
433
  });
441
- } else throw new Error("invalid request");
442
- mark("save_principal");
443
- const session = this.repository.createSession();
444
- session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);
445
- await this.repository.save(session);
446
- const response = new Response(JSON.stringify(principal), {
447
- status: 200,
448
- headers: { "Content-Type": "application/json" }
449
- });
450
- const maxAge = session.getMaxInactiveInterval();
451
- setCookie(response, this.cookieName, session.getId(), {
452
- ...this.cookieOptions,
453
- maxAge
454
- });
455
- setTiming(response);
456
- return response;
457
- };
458
- sendEmailVerificationCode = async (request, sender) => {
434
+ } else throw invalidArgument("INVALID_ARGUMENT", "credential or code is required");
435
+ return this.authorize(request, handler, {
436
+ provider,
437
+ clientId,
438
+ userInfo,
439
+ token,
440
+ attributes
441
+ }, setTiming);
442
+ });
443
+ sendEmailVerificationCode = (request, sender) => this.guard(async () => {
459
444
  invariant(this.turnstileSecretKey, "turnstileSecretKey is not configured");
460
- const { data, error } = valid(sendEmailVerificationCodeSchema, await request.json());
461
- if (error) return Response.json(error, { status: error.error.code });
462
- const { email, turnstileToken } = data;
463
- const result = await verifyTurnstileToken(this.turnstileSecretKey, turnstileToken);
464
- if (result.error) return Response.json(result.error, { status: result.error.error.code });
465
- const cooldownKey = this.getEmailVerificationCooldownKey(email);
466
- if (await this.kv.increment(cooldownKey, this.verificationResendCooldown) > 1) {
467
- const error = resourceExhausted("VERIFICATION_CODE_RESEND_TOO_SOON", "Verification code was sent recently");
468
- return Response.json(error, { status: error.error.code });
469
- }
470
- try {
471
- const verificationCode = randomInt(1e6).toString().padStart(6, "0");
472
- await sender({
473
- email,
474
- verificationCode
475
- });
476
- await this.kv.removeItem(this.getEmailVerificationAttemptsKey(email));
477
- await this.kv.setItem(this.getEmailVerificationCodeKey(email), verificationCode, EMAIL_VERIFICATION_CODE_TTL);
478
- return Response.json({});
479
- } catch (e) {
480
- console.error("Failed to send email verification code", e);
481
- await this.kv.removeItem(cooldownKey);
482
- return Response.json(invalidArgument("EMAIL_DELIVERY_FAILED"), { status: 400 });
483
- }
484
- };
485
- loginEmail = async (request, handler) => {
486
- const { data, error } = valid(loginEmailSchema, await request.json());
487
- if (error) return Response.json(error, { status: error.error.code });
488
- const { email, verificationCode, attributes } = data;
489
- const key = this.getEmailVerificationCodeKey(email);
490
- const attemptsKey = this.getEmailVerificationAttemptsKey(email);
491
- const stored = await this.kv.getItem(key);
492
- if (typeof stored !== "string") return Response.json(invalidArgument("INVALID_VERIFICATION_CODE"), { status: 400 });
493
- if (await this.kv.increment(attemptsKey, EMAIL_VERIFICATION_CODE_TTL) > this.maxVerificationAttempts) {
494
- await this.kv.removeItem(key);
495
- const error = invalidArgument("VERIFICATION_CODE_ATTEMPTS_EXCEEDED");
496
- return Response.json(error, { status: error.error.code });
497
- }
498
- if (stored.length !== verificationCode.length || !timingSafeEqual(Buffer.from(stored), Buffer.from(verificationCode))) return Response.json(invalidArgument("INVALID_VERIFICATION_CODE"), { status: 400 });
499
- await this.kv.removeItem(key);
500
- await this.kv.removeItem(attemptsKey);
501
- const principal = await handler(request, {
445
+ const { email, turnstileToken } = parse(sendEmailVerificationCodeSchema, await readJson(request));
446
+ await verifyTurnstileToken(this.turnstileSecretKey, turnstileToken);
447
+ await this.emailCodes.send(email, sender);
448
+ return Response.json({});
449
+ });
450
+ verifyEmailCode = (email, verificationCode) => this.emailCodes.verify(email, verificationCode);
451
+ loginEmail = (request, handler) => this.guard(async () => {
452
+ const { setTiming } = timing({ enabled: this.timing });
453
+ const { email, verificationCode, attributes = {} } = parse(loginEmailSchema, await readJson(request));
454
+ await this.emailCodes.verify(email, verificationCode);
455
+ const claims = {
456
+ sub: email,
457
+ email,
458
+ email_verified: true
459
+ };
460
+ return this.authorize(request, handler, {
502
461
  provider: "EMAIL",
503
462
  userInfo: {
504
- claims: {
505
- sub: email,
506
- email,
507
- email_verified: true
508
- },
463
+ claims,
509
464
  data: {}
510
465
  },
511
- attributes: attributes ?? {}
512
- });
513
- const session = this.repository.createSession();
514
- session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);
515
- await this.repository.save(session);
516
- const response = new Response(JSON.stringify(principal), {
517
- status: 200,
518
- headers: { "Content-Type": "application/json" }
519
- });
520
- const maxAge = session.getMaxInactiveInterval();
521
- setCookie(response, this.cookieName, session.getId(), {
522
- ...this.cookieOptions,
523
- maxAge
524
- });
525
- return response;
526
- };
466
+ attributes
467
+ }, setTiming);
468
+ });
527
469
  /**
528
- * Step 1 of the desktop / native app sign-in handshake. The caller already
529
- * completed normal sign-in on the web (so a cookie session exists); this
530
- * issues a 5-minute single-use code that the web page hands off to the
531
- * desktop client via a loopback redirect.
470
+ * Step 1 of the desktop / native app sign-in handshake. The caller already completed normal
471
+ * sign-in on the web (so a cookie session exists); this issues a 5-minute single-use code that
472
+ * the web page hands off to the desktop client via a loopback redirect.
532
473
  *
533
- * PKCE (RFC 7636, S256 only): the desktop generates a random `code_verifier`
534
- * before opening the browser, derives `code_challenge = base64url(sha256(verifier))`,
535
- * and threads the challenge through the URL → web page → here. We pin
536
- * the challenge to the code in KV; `desktopExchange` enforces that the
537
- * caller knows the matching verifier. This blocks an attacker who only
538
- * intercepts the auth code (browser history, malicious extension, packet
539
- * capture on loopback) from exchanging it.
474
+ * PKCE (RFC 7636, S256 only): the desktop generates a random `code_verifier` before opening the
475
+ * browser, derives `code_challenge = base64url(sha256(verifier))`, and threads the challenge
476
+ * through the URL → web page → here. The challenge is pinned to the code; `desktopExchange`
477
+ * enforces that the caller knows the matching verifier. This blocks an attacker who only
478
+ * intercepts the auth code (browser history, malicious extension, packet capture on loopback)
479
+ * from exchanging it.
540
480
  *
541
- * Requires an authenticated cookie session — anonymous callers get 401.
542
- * Throttling / abuse protection on this endpoint is the API layer's job
543
- * (rate-limit per principal); the SDK doesn't gate it because the
544
- * upstream sign-in flow already passed CAPTCHA / OAuth / verification.
481
+ * Requires an authenticated cookie session: anonymous callers get 401. Throttling / abuse
482
+ * protection on this endpoint is the API layer's job (rate-limit per principal); the SDK does
483
+ * not gate it because the upstream sign-in flow already passed CAPTCHA / OAuth / verification.
545
484
  */
546
- desktopAuthorize = async (request) => {
485
+ desktopAuthorize = (request) => this.guard(async () => {
547
486
  const principal = await this.getPrincipal(request);
548
- if (!principal) return Response.json({ error: {
549
- code: 401,
550
- status: "UNAUTHENTICATED",
551
- message: "sign in first",
552
- details: []
553
- } }, { status: 401 });
554
- let body;
555
- try {
556
- body = await request.json();
557
- } catch {
558
- return Response.json(invalidArgument("INVALID_ARGUMENT"), { status: 400 });
559
- }
560
- const codeChallenge = typeof body.code_challenge === "string" ? body.code_challenge : null;
561
- if (!codeChallenge || body.code_challenge_method !== "S256" || !PKCE_FORMAT.test(codeChallenge)) return Response.json(invalidArgument("INVALID_ARGUMENT"), { status: 400 });
562
- const code = randomUUID();
563
- const record = {
564
- name: principal.name,
565
- cc: codeChallenge
566
- };
567
- await this.kv.setItem(this.getDesktopCodeKey(code), JSON.stringify(record), 300);
487
+ if (!principal) throw unauthenticated("Sign in first");
488
+ const { code_challenge } = parse(desktopAuthorizeSchema, await readJson(request));
489
+ const code = await this.desktopCodes.issue(principal, code_challenge);
568
490
  return Response.json({ code });
569
- };
491
+ });
570
492
  /**
571
- * Step 2 of the desktop / native app sign-in handshake. The desktop
572
- * client hands back the auth code it received via the loopback callback,
573
- * and we mint a fresh session for it — independent of the web cookie
574
- * session that originally `authorize`'d the code, so logging out on
575
- * either side doesn't cascade to the other.
493
+ * Step 2 of the desktop / native app sign-in handshake. The desktop client hands back the auth
494
+ * code it received via the loopback callback, and we mint a fresh session for it, independent
495
+ * of the web cookie session that originally authorized the code, so logging out on either side
496
+ * does not cascade to the other.
576
497
  *
577
- * Cookie attributes: defaults to `SameSite=None; Secure` so the cookie
578
- * survives cross-site fetches from the desktop renderer's `file://` /
579
- * `app://` origin. The configured base `cookieOptions` provide
580
- * everything else (`domain`, `path`, `httpOnly`, `maxAge`); the override
581
- * is targeted at the SameSite/Secure pair only. In a non-HTTPS dev
582
- * backend this combination is rejected by Chromium — see the desktop
583
- * sign-in PRD for workarounds.
498
+ * Cookie attributes: `SameSite=None; Secure` so the cookie survives cross-site fetches from the
499
+ * desktop renderer's `file://` / `app://` origin. The configured `cookieOptions` provide
500
+ * everything else (`domain`, `path`, `httpOnly`); the override is targeted at the
501
+ * SameSite/Secure pair only. In a non-HTTPS dev backend this combination is rejected by
502
+ * Chromium, see the desktop sign-in PRD for workarounds.
584
503
  *
585
- * No CSRF check needed — the request carries no cookie, the auth code
586
- * itself is the credential, and KV's single-use removal blocks replay.
504
+ * No CSRF check needed: the request carries no cookie, the auth code itself is the credential,
505
+ * and the code's single-use removal blocks replay.
587
506
  */
588
- desktopExchange = async (request) => {
589
- let body;
590
- try {
591
- body = await request.json();
592
- } catch {
593
- return Response.json(invalidArgument("INVALID_ARGUMENT"), { status: 400 });
594
- }
595
- const code = typeof body.code === "string" ? body.code : null;
596
- const verifier = typeof body.code_verifier === "string" ? body.code_verifier : null;
597
- if (!code || !verifier || !PKCE_FORMAT.test(verifier)) return Response.json(invalidArgument("INVALID_ARGUMENT"), { status: 400 });
598
- const key = this.getDesktopCodeKey(code);
599
- const raw = await this.kv.getItem(key);
600
- if (typeof raw !== "string") return Response.json(invalidArgument("INVALID_DESKTOP_CODE"), { status: 400 });
601
- let record;
602
- try {
603
- record = JSON.parse(raw);
604
- } catch {
605
- await this.kv.removeItem(key);
606
- return Response.json(invalidArgument("INVALID_DESKTOP_CODE"), { status: 400 });
607
- }
608
- const computed = createHash("sha256").update(verifier).digest();
609
- const stored = Buffer.from(record.cc, "base64url");
610
- if (stored.length !== computed.length || !timingSafeEqual(computed, stored)) return Response.json(invalidArgument("INVALID_DESKTOP_CODE"), { status: 400 });
611
- await this.kv.removeItem(key);
612
- const session = this.repository.createSession();
613
- session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, record.name);
507
+ desktopExchange = (request) => this.guard(async () => {
508
+ const { code, code_verifier } = parse(desktopExchangeSchema, await readJson(request));
509
+ const principal = await this.desktopCodes.redeem(code, code_verifier);
510
+ const session = await this.signIn(principal);
614
511
  session.setAttribute("client_type", "desktop");
615
512
  await this.repository.save(session);
616
- const response = Response.json({ ok: true });
617
- const maxAge = session.getMaxInactiveInterval();
618
- setCookie(response, this.cookieName, session.getId(), {
619
- ...this.cookieOptions,
513
+ return this.withSession(Response.json({ ok: true }), session, {
620
514
  sameSite: "none",
621
- secure: true,
622
- maxAge
515
+ secure: true
623
516
  });
624
- return response;
625
- };
626
- sendPhoneVerificationCode = async (_request, _sender) => {
627
- return Response.json({ error: "not_implemented" }, { status: 501 });
628
- };
629
- loginPhone = async (_request, _handler) => {
630
- return Response.json({ error: "not_implemented" }, { status: 501 });
631
- };
517
+ });
632
518
  kick = async (principal) => {
633
519
  const sessions = await this.repository.findByPrincipalName(principal.name);
634
520
  for (const sessionId of sessions.keys()) await this.repository.deleteById(sessionId);
@@ -646,47 +532,26 @@ var Auth = class {
646
532
  session.setLastAccessedTime(now);
647
533
  await this.repository.save(session);
648
534
  }
649
- isAuthenticated = async (request) => {
650
- const sessionId = this.getSessionId(request);
651
- if (!sessionId) return false;
652
- const session = await this.repository.findById(sessionId);
653
- if (!session) return false;
654
- if (!session.getAttribute("org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME")) return false;
535
+ getPrincipal = async (request) => {
536
+ const session = await this.loadSession(request);
537
+ const principal = session && this.principalOf(session);
538
+ if (!principal) return null;
655
539
  await this.touch(session);
656
- return true;
540
+ return principal;
541
+ };
542
+ isAuthenticated = async (request) => {
543
+ return await this.getPrincipal(request) !== null;
657
544
  };
658
545
  getSession = async (request, create) => {
659
- const sessionId = this.getSessionId(request);
660
- if (!sessionId) {
661
- if (create) {
662
- const session = this.repository.createSession();
663
- await this.repository.save(session);
664
- return session;
665
- } else return null;
666
- }
667
- const session = await this.repository.findById(sessionId);
668
- if (!session) {
669
- if (create) {
670
- const session = this.repository.createSession();
671
- await this.repository.save(session);
672
- return session;
673
- } else return null;
674
- }
675
- return session;
546
+ const session = await this.loadSession(request);
547
+ if (session || !create) return session;
548
+ const created = this.repository.createSession();
549
+ await this.repository.save(created);
550
+ return created;
676
551
  };
677
552
  deleteSession = async (sessionId) => {
678
553
  await this.repository.deleteById(sessionId);
679
554
  };
680
- getPrincipal = async (request) => {
681
- const sessionId = this.getSessionId(request);
682
- if (!sessionId) return null;
683
- const session = await this.repository.findById(sessionId);
684
- if (!session) return null;
685
- const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);
686
- if (!name) return null;
687
- await this.touch(session);
688
- return { name: String(name) };
689
- };
690
555
  listSessions = async (principal) => {
691
556
  const sessions = await this.repository.findByPrincipalName(principal.name);
692
557
  return Array.from(sessions.values());