@shware/security 4.0.0 → 5.0.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.
Files changed (82) hide show
  1. package/README.md +110 -99
  2. package/dist/auth/index.cjs +44 -36
  3. package/dist/auth/index.cjs.map +1 -1
  4. package/dist/auth/index.d.cts +6 -6
  5. package/dist/auth/index.d.cts.map +1 -1
  6. package/dist/auth/index.d.mts +6 -6
  7. package/dist/auth/index.d.mts.map +1 -1
  8. package/dist/auth/index.mjs +44 -36
  9. package/dist/auth/index.mjs.map +1 -1
  10. package/dist/auth/types.d.cts +12 -7
  11. package/dist/auth/types.d.cts.map +1 -1
  12. package/dist/auth/types.d.mts +12 -7
  13. package/dist/auth/types.d.mts.map +1 -1
  14. package/dist/index.cjs +4 -1
  15. package/dist/index.d.cts +3 -2
  16. package/dist/index.d.mts +3 -2
  17. package/dist/index.mjs +3 -2
  18. package/dist/oauth2/client.cjs +113 -49
  19. package/dist/oauth2/client.cjs.map +1 -1
  20. package/dist/oauth2/client.d.cts +54 -14
  21. package/dist/oauth2/client.d.cts.map +1 -1
  22. package/dist/oauth2/client.d.mts +54 -14
  23. package/dist/oauth2/client.d.mts.map +1 -1
  24. package/dist/oauth2/client.mjs +113 -49
  25. package/dist/oauth2/client.mjs.map +1 -1
  26. package/dist/oauth2/name.cjs +107 -0
  27. package/dist/oauth2/name.cjs.map +1 -0
  28. package/dist/oauth2/name.d.cts +49 -0
  29. package/dist/oauth2/name.d.cts.map +1 -0
  30. package/dist/oauth2/name.d.mts +49 -0
  31. package/dist/oauth2/name.d.mts.map +1 -0
  32. package/dist/oauth2/name.mjs +101 -0
  33. package/dist/oauth2/name.mjs.map +1 -0
  34. package/dist/oauth2/provider/apple-client-secret.cjs +3 -3
  35. package/dist/oauth2/provider/apple-client-secret.cjs.map +1 -1
  36. package/dist/oauth2/provider/apple-client-secret.d.cts +3 -3
  37. package/dist/oauth2/provider/apple-client-secret.d.mts +3 -3
  38. package/dist/oauth2/provider/apple-client-secret.mjs +3 -3
  39. package/dist/oauth2/provider/apple-client-secret.mjs.map +1 -1
  40. package/dist/oauth2/provider/apple.cjs +38 -10
  41. package/dist/oauth2/provider/apple.cjs.map +1 -1
  42. package/dist/oauth2/provider/apple.d.cts +19 -14
  43. package/dist/oauth2/provider/apple.d.cts.map +1 -1
  44. package/dist/oauth2/provider/apple.d.mts +19 -14
  45. package/dist/oauth2/provider/apple.d.mts.map +1 -1
  46. package/dist/oauth2/provider/apple.mjs +38 -10
  47. package/dist/oauth2/provider/apple.mjs.map +1 -1
  48. package/dist/oauth2/provider/common.cjs +13 -9
  49. package/dist/oauth2/provider/common.cjs.map +1 -1
  50. package/dist/oauth2/provider/common.d.cts +13 -2
  51. package/dist/oauth2/provider/common.d.cts.map +1 -1
  52. package/dist/oauth2/provider/common.d.mts +13 -2
  53. package/dist/oauth2/provider/common.d.mts.map +1 -1
  54. package/dist/oauth2/provider/common.mjs +13 -9
  55. package/dist/oauth2/provider/common.mjs.map +1 -1
  56. package/dist/oauth2/provider/facebook.cjs +59 -35
  57. package/dist/oauth2/provider/facebook.cjs.map +1 -1
  58. package/dist/oauth2/provider/facebook.d.cts.map +1 -1
  59. package/dist/oauth2/provider/facebook.d.mts.map +1 -1
  60. package/dist/oauth2/provider/facebook.mjs +59 -35
  61. package/dist/oauth2/provider/facebook.mjs.map +1 -1
  62. package/dist/oauth2/provider/google.cjs +31 -32
  63. package/dist/oauth2/provider/google.cjs.map +1 -1
  64. package/dist/oauth2/provider/google.d.cts.map +1 -1
  65. package/dist/oauth2/provider/google.d.mts.map +1 -1
  66. package/dist/oauth2/provider/google.mjs +31 -32
  67. package/dist/oauth2/provider/google.mjs.map +1 -1
  68. package/dist/oauth2/provider/index.cjs +1 -1
  69. package/dist/oauth2/provider/index.mjs +1 -1
  70. package/dist/oauth2/provider/wechat.cjs +1 -0
  71. package/dist/oauth2/provider/wechat.cjs.map +1 -1
  72. package/dist/oauth2/provider/wechat.d.cts.map +1 -1
  73. package/dist/oauth2/provider/wechat.d.mts.map +1 -1
  74. package/dist/oauth2/provider/wechat.mjs +1 -0
  75. package/dist/oauth2/provider/wechat.mjs.map +1 -1
  76. package/dist/oauth2/types.cjs.map +1 -1
  77. package/dist/oauth2/types.d.cts +100 -33
  78. package/dist/oauth2/types.d.cts.map +1 -1
  79. package/dist/oauth2/types.d.mts +100 -33
  80. package/dist/oauth2/types.d.mts.map +1 -1
  81. package/dist/oauth2/types.mjs.map +1 -1
  82. package/package.json +1 -1
package/README.md CHANGED
@@ -26,7 +26,7 @@ use the Redis repositories.
26
26
 
27
27
  ```ts
28
28
  import { Auth, PATH, RedisIndexedSessionRepository, RedisKVRepository } from '@shware/security';
29
- import { apple, github } from '@shware/security/oauth2/provider';
29
+ import { apple, createAppleClientSecret, github, google } from '@shware/security/oauth2/provider';
30
30
  import { Hono } from 'hono';
31
31
  import { Redis } from 'ioredis';
32
32
  import { env } from './env';
@@ -38,6 +38,12 @@ const redis = new Redis(env.REDIS_URL);
38
38
  const sessionRepository = new RedisIndexedSessionRepository(redis, 'mywebsite:session');
39
39
  sessionRepository.setDefaultMaxInactiveInterval(30 * 24 * 60 * 60); // seconds
40
40
 
41
+ const appleClientSecret = createAppleClientSecret({
42
+ teamId: env.OAUTH2_APPLE_TEAM_ID,
43
+ keyId: env.OAUTH2_APPLE_KEY_ID,
44
+ privateKey: env.OAUTH2_APPLE_P8, // PKCS#8 PEM contents of the .p8 key
45
+ });
46
+
41
47
  const auth = new Auth({
42
48
  timing: true,
43
49
  sessionRepository,
@@ -46,24 +52,30 @@ const auth = new Auth({
46
52
  client: {
47
53
  baseUri: 'https://api.mywebsite.com', // backend, used to build redirect URIs
48
54
  errorUri: 'https://mywebsite.com', // receives ?error=...&error_description=...
49
- successUri: 'https://mywebsite.com', // receives ?status=success&registrationId=...
50
- provider: { apple, github },
51
- registration: async () => ({
52
- appleApp: {
53
- provider: 'apple',
54
- clientId: env.OAUTH2_APPLE_APP_CLIENT_ID,
55
- clientSecret: env.OAUTH2_APPLE_APP_CLIENT_SECRET,
55
+ successUri: 'https://mywebsite.com', // receives ?status=success&provider=...
56
+ providers: {
57
+ apple: {
58
+ provider: apple,
59
+ web: { clientId: env.OAUTH2_APPLE_SERVICES_ID, clientSecret: appleClientSecret },
60
+ native: [{ clientId: env.OAUTH2_APPLE_BUNDLE_ID, clientSecret: appleClientSecret }],
56
61
  },
57
- appleWeb: {
58
- provider: 'apple',
59
- clientId: env.OAUTH2_APPLE_WEB_CLIENT_ID,
60
- clientSecret: env.OAUTH2_APPLE_WEB_CLIENT_SECRET,
62
+ google: {
63
+ provider: google,
64
+ web: {
65
+ clientId: env.OAUTH2_GOOGLE_WEB_CLIENT_ID,
66
+ clientSecret: env.OAUTH2_GOOGLE_SECRET,
67
+ },
68
+ // installed-app clients are public: no secret
69
+ native: [
70
+ { clientId: env.OAUTH2_GOOGLE_IOS_CLIENT_ID },
71
+ { clientId: env.OAUTH2_GOOGLE_ANDROID_CLIENT_ID },
72
+ ],
61
73
  },
62
74
  github: {
63
- clientId: env.OAUTH2_GITHUB_CLIENT_ID,
64
- clientSecret: env.OAUTH2_GITHUB_CLIENT_SECRET,
75
+ provider: github,
76
+ web: { clientId: env.OAUTH2_GITHUB_CLIENT_ID, clientSecret: env.OAUTH2_GITHUB_SECRET },
65
77
  },
66
- }),
78
+ },
67
79
  },
68
80
  },
69
81
  });
@@ -85,32 +97,53 @@ app.post(PATH.LOGIN_OAUTH2_ONETAP, (c) => auth.loginOAuth2Onetap(c.req.raw, onAu
85
97
  export default app;
86
98
  ```
87
99
 
88
- A registration whose key is not a provider name (`appleApp`, `appleWeb`) sets `provider`; otherwise
89
- the key itself is used (`github`). The key is the `:registrationId` in the routes.
100
+ ### Providers and clients
101
+
102
+ `providers` is keyed by **identity source**: one key per namespace of `sub`. Its key is the
103
+ `:provider` in the routes and `authorization.provider` in `onAuthorized`. Each entry names the
104
+ implementation (`provider`) and the clients users sign in through:
105
+
106
+ - `web`: the client of the redirect flow and of Google One Tap (a Google web client, an Apple
107
+ Services ID, a GitHub OAuth app). `redirectUri` defaults to
108
+ `${baseUri}/login/oauth2/code/<provider key>`; `scope` overrides the implementation's defaults.
109
+ - `native`: the clients apps sign in as on the device (an iOS bundle id, Google's iOS / Android
110
+ clients). The app sends its `client_id` with the native login, which must be one of the
111
+ provider's clients (`web` included); without one, the only `native` client is used, or `web`
112
+ when there is no `native` one. Several `native` clients and no `client_id` is refused.
113
+
114
+ `clientSecret` is a string, a function `clientId => secret` called on each use with that client's
115
+ id (`createAppleClientSecret` returns one), or absent for a public client. `providers` may also be
116
+ an async function, called per request.
117
+
118
+ Two keys may share an implementation when their users are separate namespaces, e.g. two Google
119
+ Cloud projects: `{ google: { provider: google, ... }, googleWorkspace: { provider: google, ... } }`.
90
120
 
91
121
  ### The `onAuthorized` handler
92
122
 
93
123
  Every login method takes an `AuthorizedHandler`: create or update your user and return the
94
124
  `Principal` whose `name` is stored in the session. It gets the request and an `Authorization`
95
- object — `{ registrationId, userInfo, token?, attributes }` — so fields are named, not positional.
125
+ object — `{ provider, clientId?, userInfo, token?, attributes }` — so fields are named, not
126
+ positional. `provider` plus `userInfo.claims.sub` identify the user, whichever client they came
127
+ through; `clientId` tells the surfaces apart (absent for email sign-in, whose `provider` is
128
+ `EMAIL`).
96
129
 
97
130
  ```ts
98
131
  import type { Authorization, Principal, Provider } from '@shware/security';
99
132
 
100
- const PROVIDER_BY_REGISTRATION: Record<string, Provider | undefined> = {
133
+ const PROVIDERS: Record<string, Provider | undefined> = {
134
+ apple: 'APPLE',
135
+ google: 'GOOGLE',
101
136
  github: 'GITHUB',
102
- appleApp: 'APPLE',
103
- appleWeb: 'APPLE',
104
- EMAIL: 'EMAIL', // registration id used by loginEmail
137
+ EMAIL: 'EMAIL', // provider used by loginEmail
105
138
  };
106
139
 
107
140
  export async function onAuthorized(
108
141
  request: Request,
109
- { registrationId, userInfo: { claims } }: Authorization
142
+ { provider, userInfo: { claims } }: Authorization
110
143
  ): Promise<Principal> {
111
- const provider = PROVIDER_BY_REGISTRATION[registrationId];
112
- if (!provider) throw new Error(`Unknown registration: ${registrationId}`);
113
- const userId = await upsertUser(provider, claims.sub, claims.email);
144
+ const method = PROVIDERS[provider];
145
+ if (!method) throw new Error(`Unknown provider: ${provider}`);
146
+ const userId = await upsertUser(method, claims.sub, claims.email);
114
147
  return { name: userId };
115
148
  }
116
149
  ```
@@ -125,7 +158,7 @@ handy for a database enum.
125
158
  | `sessionRepository` | `SessionRepository` | Required. Where sessions live (see below). |
126
159
  | `kvRepository` | `KVRepository` | Required. Short-lived values: OAuth2 state/nonce, verification and desktop codes. |
127
160
  | `cookie` | `CookieOptions & { name?: string }` | Session cookie. Defaults: name `SESSION`, `path: '/'`, `SameSite=None`, `Secure`, `HttpOnly`. |
128
- | `oauth2.client` | `OAuth2ClientConfig` | `baseUri`, `errorUri`, `successUri`, `provider`, `registration` (object or async function). |
161
+ | `oauth2.client` | `OAuth2ClientConfig` | `baseUri`, `errorUri`, `successUri`, `providers` (object or async function). |
129
162
  | `turnstileSecretKey` | `string` | Cloudflare Turnstile secret; required by `sendEmailVerificationCode`. |
130
163
  | `timing` | `boolean` | Adds a `Server-Timing` header to OAuth2 responses. |
131
164
 
@@ -242,56 +275,34 @@ export const auth = new Auth({ sessionRepository, kvRepository: new DBKVReposito
242
275
  | `x` | X (Twitter) OAuth 2.0 |
243
276
 
244
277
  Apple has no static client secret; `createAppleClientSecret` signs one per client id and caches it
245
- until shortly before it expires, so you can mint it inside an async `registration` function:
278
+ until shortly before it expires. Pass the function itself as the `clientSecret` of every Apple
279
+ client, as in the quick start: the app's bundle id and the web's Services ID share the key and get
280
+ a secret each.
246
281
 
247
- ```ts
248
- import { apple, createAppleClientSecret, github, google } from '@shware/security/oauth2/provider';
282
+ ### Id token verification
249
283
 
250
- const appleClientSecret = createAppleClientSecret({
251
- teamId: env.OAUTH2_APPLE_TEAM_ID,
252
- keyId: env.OAUTH2_APPLE_KEY_ID,
253
- privateKey: env.OAUTH2_APPLE_P8, // PKCS#8 PEM contents of the .p8 key
254
- });
284
+ Every id token (Apple, Google, Facebook Limited Login) is checked for its signature, expiry,
285
+ issuer and audience. The audience must be the client the token was meant for: the client the
286
+ code was exchanged as, the client the native login resolved, or the web client for One Tap. A
287
+ token issued to any other app on the same identity provider is refused.
255
288
 
256
- const auth = new Auth({
257
- sessionRepository,
258
- kvRepository,
259
- oauth2: {
260
- client: {
261
- baseUri: 'https://api.mywebsite.com',
262
- errorUri: 'https://mywebsite.com?status=error',
263
- successUri: 'https://mywebsite.com?status=success',
264
- provider: { apple, github, google },
265
- registration: async () => ({
266
- github: { clientId: env.GITHUB_CLIENT_ID, clientSecret: env.GITHUB_CLIENT_SECRET },
267
- google: { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET },
268
- // native app: its bundle id
269
- apple: {
270
- clientId: env.APPLE_APP_CLIENT_ID,
271
- clientSecret: await appleClientSecret(env.APPLE_APP_CLIENT_ID),
272
- },
273
- // web: its Services ID
274
- appleWeb: {
275
- provider: 'apple',
276
- clientId: env.APPLE_WEB_CLIENT_ID,
277
- clientSecret: await appleClientSecret(env.APPLE_WEB_CLIENT_ID),
278
- },
279
- }),
280
- },
281
- },
282
- });
283
- ```
289
+ | Implementation | Accepted `iss` |
290
+ | -------------- | ---------------------------------------------------- |
291
+ | `apple` | `https://appleid.apple.com` |
292
+ | `google` | `https://accounts.google.com`, `accounts.google.com` |
293
+ | `facebook` | `https://www.facebook.com` |
284
294
 
285
- The redirect URI defaults to `${baseUri}/login/oauth2/code/<registrationId>`; set
286
- `redirectUri` on a registration to override it, and `scope` to override the provider's defaults.
295
+ Facebook's native login also takes a classic `access_token`, which the Graph API would serve to
296
+ any app; it is sent with an `appsecret_proof`, so the client needs its `clientSecret`.
287
297
 
288
298
  ### Web redirect flow
289
299
 
290
- 1. The browser opens `GET /oauth2/authorization/:registrationId`. `oauth2Authorization` stores the
300
+ 1. The browser opens `GET /oauth2/authorization/:provider`. `oauth2Authorization` stores the
291
301
  state and PKCE verifier in the session and redirects to the provider.
292
- 2. The provider calls back `GET|POST /login/oauth2/code/:registrationId` (Apple uses `form_post`,
293
- so register both). `loginOAuth2Code` checks state, exchanges the code, calls `onAuthorized`
294
- and redirects to `successUri`, or to `errorUri` with `error` and `error_description`.
302
+ 2. The provider calls back `GET|POST /login/oauth2/code/:provider` (Apple uses `form_post`,
303
+ so register both). `loginOAuth2Code` checks state, exchanges the code as the `web` client,
304
+ calls `onAuthorized` and redirects to `successUri` with `status=success&provider=<key>`, or to
305
+ `errorUri` with `error` and `error_description`.
295
306
 
296
307
  #### Carrying data to `onAuthorized` (`attributes`)
297
308
 
@@ -317,7 +328,7 @@ export async function onAuthorized(request: Request, { attributes }: Authorizati
317
328
  - Stored with the authorization request in the session (the same JSON as `state` and the PKCE
318
329
  verifier), never sent to the provider, so no extra storage, expiry or cleanup.
319
330
  - Handed to `onAuthorized` as `authorization.attributes` only after the session, `state` and
320
- registration check out, and removed with the authorization request after a successful login,
331
+ provider check out, and removed with the authorization request after a successful login,
321
332
  so they are used once.
322
333
  - String values only; `undefined` values are dropped; at most 2 KB serialized (they are written
323
334
  with the session).
@@ -331,15 +342,15 @@ export async function onAuthorized(request: Request, { attributes }: Authorizati
331
342
  ### Native login
332
343
 
333
344
  Mobile apps sign in with the platform SDK and post the result to
334
- `POST /login/oauth2/native/:registrationId`. The body is a `NativeCredential`
335
- (`{ id_token?, access_token?, code?, state?, client_id?, redirect_uri? }`). `loginOAuth2Native`
345
+ `POST /login/oauth2/native/:provider`. The body is a `NativeCredential`
346
+ (`{ id_token?, access_token?, code?, state?, client_id?, redirect_uri?, fullName? }`); `client_id`
347
+ picks the client (see [Providers and clients](#providers-and-clients)). `loginOAuth2Native`
336
348
  verifies it with the provider, calls `onAuthorized`, creates a session and returns the
337
349
  `Principal` as JSON with the session cookie.
338
350
 
339
- To bind an Apple `id_token` to this request, fetch a nonce first from
340
- `GET /oauth2/nonce/:registrationId` (`oauth2Nonce`) and pass it to the Apple SDK; when the token
341
- carries a `nonce`, `loginOAuth2Native` requires it to match one issued for the same registration
342
- and consumes it.
351
+ To bind an id token to this request, fetch a nonce first from `GET /oauth2/nonce/:provider`
352
+ (`oauth2Nonce`) and pass it to the platform SDK; when the token carries a `nonce`,
353
+ `loginOAuth2Native` requires it to match one issued for the same provider and consumes it.
343
354
 
344
355
  ```ts
345
356
  const { nonce } = await fetch(`${api}/oauth2/nonce/apple`).then((r) => r.json());
@@ -347,15 +358,15 @@ const { nonce } = await fetch(`${api}/oauth2/nonce/apple`).then((r) => r.json())
347
358
  await fetch(`${api}/login/oauth2/native/apple`, {
348
359
  method: 'POST',
349
360
  headers: { 'Content-Type': 'application/json' },
350
- body: JSON.stringify({ id_token: identityToken }),
361
+ body: JSON.stringify({ code: authorizationCode, fullName: credential.fullName }),
351
362
  });
352
363
  ```
353
364
 
354
365
  ### Google One Tap
355
366
 
356
367
  `POST /login/oauth2/onetap/google` accepts the One Tap `credential` (an `id_token`) or a `code`,
357
- as form data or JSON. The third argument of `loginOAuth2Onetap` is the registration id (default
358
- `'google'`).
368
+ as form data or JSON, both issued to the `web` client of the provider. The third argument of
369
+ `loginOAuth2Onetap` is the provider key (default `'google'`).
359
370
 
360
371
  ## Email sign-in
361
372
 
@@ -379,33 +390,33 @@ app.post(PATH.LOGIN_EMAIL, (c) => auth.loginEmail(c.req.raw, onAuthorized));
379
390
 
380
391
  - `POST /auth/sendEmailVerificationCode` with `{ email, turnstileToken }`.
381
392
  - `POST /login/email` with `{ email, verificationCode }`. On success `onAuthorized` is called with
382
- `registrationId: 'EMAIL'`, claims `{ sub: email, email, email_verified: true }` and no `token`.
393
+ `provider: 'EMAIL'`, claims `{ sub: email, email, email_verified: true }` and no `token`.
383
394
 
384
395
  `sendPhoneVerificationCode` and `loginPhone` exist but currently answer `501 not_implemented`.
385
396
 
386
397
  ## Routes (`PATH`)
387
398
 
388
- `PATH` holds the route patterns the handlers parse `:registrationId` from, so mount each handler on
399
+ `PATH` holds the route patterns the handlers parse `:provider` from, so mount each handler on
389
400
  its own constant.
390
401
 
391
- | Constant | Path | Handler |
392
- | ------------------------------ | --------------------------------------- | ------------------------------------------------- |
393
- | `CSRF` | `/csrf` | `csrf` (double-submit `XSRF-TOKEN` cookie) |
394
- | `LOGOUT` | `/logout` | `logout` |
395
- | `LOGGED` | `/logged` | `logged` (`{ data: boolean }`, renews the cookie) |
396
- | `OAUTH2_STATE` | `/oauth2/state/:registrationId` | `oauth2State` |
397
- | `OAUTH2_NONCE` | `/oauth2/nonce/:registrationId` | `oauth2Nonce` |
398
- | `OAUTH2_AUTHORIZATION` | `/oauth2/authorization/:registrationId` | `oauth2Authorization` |
399
- | `LOGIN_OAUTH2_CODE` | `/login/oauth2/code/:registrationId` | `loginOAuth2Code` (GET and POST) |
400
- | `LOGIN_OAUTH2_NATIVE` | `/login/oauth2/native/:registrationId` | `loginOAuth2Native` |
401
- | `LOGIN_OAUTH2_ONETAP` | `/login/oauth2/onetap/google` | `loginOAuth2Onetap` |
402
- | `LOGIN_EMAIL` | `/login/email` | `loginEmail` |
403
- | `LOGIN_PHONE` | `/login/phone` | `loginPhone` (not implemented) |
404
- | `SEND_EMAIL_VERIFICATION_CODE` | `/auth/sendEmailVerificationCode` | `sendEmailVerificationCode` |
405
- | `SEND_PHONE_VERIFICATION_CODE` | `/auth/sendPhoneVerificationCode` | `sendPhoneVerificationCode` (not implemented) |
406
- | `DESKTOP_AUTHORIZE` | `/auth/desktop/authorize` | `desktopAuthorize` |
407
- | `DESKTOP_EXCHANGE` | `/auth/desktop/exchange` | `desktopExchange` |
408
- | `CLEANUP_EXPIRED_SESSIONS` | `/sessions/expired/cleanup` | `cleanupExpiredSessions` |
402
+ | Constant | Path | Handler |
403
+ | ------------------------------ | --------------------------------- | ------------------------------------------------- |
404
+ | `CSRF` | `/csrf` | `csrf` (double-submit `XSRF-TOKEN` cookie) |
405
+ | `LOGOUT` | `/logout` | `logout` |
406
+ | `LOGGED` | `/logged` | `logged` (`{ data: boolean }`, renews the cookie) |
407
+ | `OAUTH2_STATE` | `/oauth2/state/:provider` | `oauth2State` |
408
+ | `OAUTH2_NONCE` | `/oauth2/nonce/:provider` | `oauth2Nonce` |
409
+ | `OAUTH2_AUTHORIZATION` | `/oauth2/authorization/:provider` | `oauth2Authorization` |
410
+ | `LOGIN_OAUTH2_CODE` | `/login/oauth2/code/:provider` | `loginOAuth2Code` (GET and POST) |
411
+ | `LOGIN_OAUTH2_NATIVE` | `/login/oauth2/native/:provider` | `loginOAuth2Native` |
412
+ | `LOGIN_OAUTH2_ONETAP` | `/login/oauth2/onetap/google` | `loginOAuth2Onetap` |
413
+ | `LOGIN_EMAIL` | `/login/email` | `loginEmail` |
414
+ | `LOGIN_PHONE` | `/login/phone` | `loginPhone` (not implemented) |
415
+ | `SEND_EMAIL_VERIFICATION_CODE` | `/auth/sendEmailVerificationCode` | `sendEmailVerificationCode` |
416
+ | `SEND_PHONE_VERIFICATION_CODE` | `/auth/sendPhoneVerificationCode` | `sendPhoneVerificationCode` (not implemented) |
417
+ | `DESKTOP_AUTHORIZE` | `/auth/desktop/authorize` | `desktopAuthorize` |
418
+ | `DESKTOP_EXCHANGE` | `/auth/desktop/exchange` | `desktopExchange` |
419
+ | `CLEANUP_EXPIRED_SESSIONS` | `/sessions/expired/cleanup` | `cleanupExpiredSessions` |
409
420
 
410
421
  `cleanupExpiredSessions` has no authentication of its own; prefer calling it from a scheduled job
411
422
  over exposing the route.
@@ -2,8 +2,6 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_auth_attributes = require("./attributes.cjs");
3
3
  const require_email_schema = require("../email/schema.cjs");
4
4
  const require_oauth2_client = require("../oauth2/client.cjs");
5
- const require_oauth2_provider_google = require("../oauth2/provider/google.cjs");
6
- require("../oauth2/provider/index.cjs");
7
5
  const require_session_common = require("../session/common.cjs");
8
6
  const require_utils_http = require("../utils/http.cjs");
9
7
  const require_utils_timing = require("../utils/timing.cjs");
@@ -34,11 +32,11 @@ const PATH = {
34
32
  CSRF: "/csrf",
35
33
  LOGOUT: "/logout",
36
34
  LOGGED: "/logged",
37
- OAUTH2_STATE: "/oauth2/state/:registrationId",
38
- OAUTH2_NONCE: "/oauth2/nonce/:registrationId",
39
- OAUTH2_AUTHORIZATION: "/oauth2/authorization/:registrationId",
40
- LOGIN_OAUTH2_CODE: "/login/oauth2/code/:registrationId",
41
- LOGIN_OAUTH2_NATIVE: "/login/oauth2/native/:registrationId",
35
+ OAUTH2_STATE: "/oauth2/state/:provider",
36
+ OAUTH2_NONCE: "/oauth2/nonce/:provider",
37
+ OAUTH2_AUTHORIZATION: "/oauth2/authorization/:provider",
38
+ LOGIN_OAUTH2_CODE: "/login/oauth2/code/:provider",
39
+ LOGIN_OAUTH2_NATIVE: "/login/oauth2/native/:provider",
42
40
  LOGIN_OAUTH2_ONETAP: "/login/oauth2/onetap/google",
43
41
  LOGIN_EMAIL: "/login/email",
44
42
  LOGIN_PHONE: "/login/phone",
@@ -144,26 +142,26 @@ var Auth = class {
144
142
  return `desktop:code:${code}`;
145
143
  }
146
144
  oauth2State = async (request) => {
147
- const { registrationId } = require_utils_http.param(request, PATH.OAUTH2_STATE);
145
+ const { provider } = require_utils_http.param(request, PATH.OAUTH2_STATE);
148
146
  const state = (0, node_crypto.randomUUID)();
149
- await this.kv.setItem(this.getOauth2StateKey(state), registrationId, 600);
147
+ await this.kv.setItem(this.getOauth2StateKey(state), provider, 600);
150
148
  return Response.json({ state });
151
149
  };
152
150
  oauth2Nonce = async (request) => {
153
- const { registrationId } = require_utils_http.param(request, PATH.OAUTH2_NONCE);
151
+ const { provider } = require_utils_http.param(request, PATH.OAUTH2_NONCE);
154
152
  const nonce = (0, node_crypto.randomUUID)();
155
- await this.kv.setItem(this.getOauth2NonceKey(nonce), registrationId, 600);
153
+ await this.kv.setItem(this.getOauth2NonceKey(nonce), provider, 600);
156
154
  return Response.json({ nonce });
157
155
  };
158
156
  oauth2Authorization = async (request, options) => {
159
157
  (0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
160
158
  const attributes = authorizationAttributes(options?.attributes);
161
159
  const { mark, setTiming } = require_utils_timing.timing({ enabled: this.timing });
162
- const { registrationId } = require_utils_http.param(request, PATH.OAUTH2_AUTHORIZATION);
160
+ const { provider } = require_utils_http.param(request, PATH.OAUTH2_AUTHORIZATION);
163
161
  const state = (0, node_crypto.randomUUID)();
164
162
  const pkce = this.createPkceParameters();
165
163
  const uri = await this.oauth2Client.createAuthorizationUri({
166
- registrationId,
164
+ provider,
167
165
  state,
168
166
  pkce
169
167
  });
@@ -172,7 +170,7 @@ var Auth = class {
172
170
  mark("fetch_session");
173
171
  const authorizationRequest = {
174
172
  state,
175
- registrationId,
173
+ provider,
176
174
  authorizationRequestUri: uri.href,
177
175
  additionalParameters: pkce,
178
176
  ...attributes ? { attributes } : {}
@@ -210,7 +208,7 @@ var Auth = class {
210
208
  const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);
211
209
  if (!json) return this.redirect("invalid_request", "authorization request not found");
212
210
  const cached = JSON.parse(json);
213
- const { registrationId } = require_utils_http.param(request, PATH.LOGIN_OAUTH2_CODE);
211
+ const { provider } = require_utils_http.param(request, PATH.LOGIN_OAUTH2_CODE);
214
212
  let data;
215
213
  if (isFormPost(request)) {
216
214
  const params = new URLSearchParams(await request.text());
@@ -220,21 +218,24 @@ var Auth = class {
220
218
  if (!parsed.success) return this.redirect("invalid_request", "invalid redirect query");
221
219
  if (!parsed.data.code || !parsed.data.state) return this.redirect("invalid_request", "invalid redirect query");
222
220
  if (parsed.data.state !== cached.state) return this.redirect("invalid_request", "redirect state mismatch");
223
- if (registrationId !== cached.registrationId) return this.redirect("invalid_request", "redirect registration mismatch");
221
+ if (provider !== cached.provider) return this.redirect("invalid_request", "redirect provider mismatch");
224
222
  mark("validate_session");
225
- const { code } = parsed.data;
226
- const token = await this.oauth2Client.exchangeAuthorizationCode({
227
- registrationId,
223
+ const { code, user } = parsed.data;
224
+ const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({
225
+ provider,
228
226
  code,
229
227
  pkce: cached.additionalParameters
230
228
  });
231
229
  const userInfo = await this.oauth2Client.getUserInfo({
232
- registrationId,
233
- token
230
+ provider,
231
+ clientId,
232
+ token,
233
+ response: user ? { user } : void 0
234
234
  });
235
235
  mark("exchange_code");
236
236
  const principal = await handler(request, {
237
- registrationId,
237
+ provider,
238
+ clientId,
238
239
  userInfo,
239
240
  token,
240
241
  attributes: cached.attributes ?? {}
@@ -247,7 +248,7 @@ var Auth = class {
247
248
  mark("update_session");
248
249
  const uri = new URL(this.oauth2Client.successUri);
249
250
  uri.searchParams.set("status", "success");
250
- uri.searchParams.set("registrationId", registrationId);
251
+ uri.searchParams.set("provider", provider);
251
252
  const response = new Response(null, {
252
253
  status: 302,
253
254
  headers: { location: uri.href }
@@ -263,19 +264,19 @@ var Auth = class {
263
264
  loginOAuth2Native = async (request, handler) => {
264
265
  (0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
265
266
  const { mark, setTiming } = require_utils_timing.timing({ enabled: this.timing });
266
- const { registrationId } = require_utils_http.param(request, PATH.LOGIN_OAUTH2_NATIVE);
267
+ const { provider } = require_utils_http.param(request, PATH.LOGIN_OAUTH2_NATIVE);
267
268
  const body = await request.json();
268
269
  const attributes = require_auth_attributes.bodyAttributes(body);
269
270
  if (!attributes) return Response.json(INVALID_ATTRIBUTES, { status: 400 });
270
271
  const { attributes: _, ...credentials } = body;
271
- const { userInfo, token } = await this.oauth2Client.loginOAuth2Native({
272
- registrationId,
272
+ const { clientId, userInfo, token } = await this.oauth2Client.loginOAuth2Native({
273
+ provider,
273
274
  credentials
274
275
  });
275
276
  mark("login_oauth2_native");
276
277
  const { nonce } = userInfo.data;
277
278
  if (nonce) {
278
- if (await this.kv.getItem(this.getOauth2NonceKey(nonce)) !== registrationId) return Response.json({
279
+ if (await this.kv.getItem(this.getOauth2NonceKey(nonce)) !== provider) return Response.json({
279
280
  error: "invalid_request",
280
281
  error_description: "nonce mismatch"
281
282
  }, { status: 400 });
@@ -283,7 +284,8 @@ var Auth = class {
283
284
  mark("check_nonce");
284
285
  }
285
286
  const principal = await handler(request, {
286
- registrationId,
287
+ provider,
288
+ clientId,
287
289
  userInfo,
288
290
  token,
289
291
  attributes
@@ -304,7 +306,7 @@ var Auth = class {
304
306
  setTiming(response);
305
307
  return response;
306
308
  };
307
- loginOAuth2Onetap = async (request, handler, registrationId = "google") => {
309
+ loginOAuth2Onetap = async (request, handler, provider = "google") => {
308
310
  (0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
309
311
  const { mark, setTiming } = require_utils_timing.timing({ enabled: this.timing });
310
312
  let body;
@@ -324,22 +326,28 @@ var Auth = class {
324
326
  const parsed = require_oauth2_client.googleOneTapSchema.parse(body);
325
327
  let principal;
326
328
  if (parsed.credential) {
327
- const { token, userInfo } = await require_oauth2_provider_google.google.getTokenInfo(parsed.credential);
329
+ const { clientId, token, userInfo } = await this.oauth2Client.getTokenInfo({
330
+ provider,
331
+ credential: parsed.credential
332
+ });
328
333
  principal = await handler(request, {
329
- registrationId,
334
+ provider,
335
+ clientId,
330
336
  userInfo,
331
337
  token,
332
338
  attributes
333
339
  });
334
340
  } else if (parsed.code) {
335
- const token = await this.oauth2Client.exchangeAuthorizationCode({
336
- registrationId,
341
+ const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({
342
+ provider,
337
343
  code: parsed.code
338
344
  });
339
345
  principal = await handler(request, {
340
- registrationId,
346
+ provider,
347
+ clientId,
341
348
  userInfo: await this.oauth2Client.getUserInfo({
342
- registrationId,
349
+ provider,
350
+ clientId,
343
351
  token
344
352
  }),
345
353
  token,
@@ -391,7 +399,7 @@ var Auth = class {
391
399
  if (typeof stored !== "string" || stored.length !== verificationCode.length || !(0, node_crypto.timingSafeEqual)(Buffer.from(stored), Buffer.from(verificationCode))) return Response.json(require_utils_valid.invalidArgument("INVALID_VERIFICATION_CODE"), { status: 400 });
392
400
  await this.kv.removeItem(key);
393
401
  const principal = await handler(request, {
394
- registrationId: "EMAIL",
402
+ provider: "EMAIL",
395
403
  userInfo: {
396
404
  claims: {
397
405
  sub: email,