@shware/security 4.1.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.
- package/README.md +110 -99
- package/dist/auth/index.cjs +41 -34
- package/dist/auth/index.cjs.map +1 -1
- package/dist/auth/index.d.cts +6 -6
- package/dist/auth/index.d.cts.map +1 -1
- package/dist/auth/index.d.mts +6 -6
- package/dist/auth/index.d.mts.map +1 -1
- package/dist/auth/index.mjs +41 -34
- package/dist/auth/index.mjs.map +1 -1
- package/dist/auth/types.d.cts +12 -7
- package/dist/auth/types.d.cts.map +1 -1
- package/dist/auth/types.d.mts +12 -7
- package/dist/auth/types.d.mts.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/oauth2/client.cjs +111 -48
- package/dist/oauth2/client.cjs.map +1 -1
- package/dist/oauth2/client.d.cts +51 -13
- package/dist/oauth2/client.d.cts.map +1 -1
- package/dist/oauth2/client.d.mts +51 -13
- package/dist/oauth2/client.d.mts.map +1 -1
- package/dist/oauth2/client.mjs +111 -48
- package/dist/oauth2/client.mjs.map +1 -1
- package/dist/oauth2/provider/apple-client-secret.cjs +3 -3
- package/dist/oauth2/provider/apple-client-secret.cjs.map +1 -1
- package/dist/oauth2/provider/apple-client-secret.d.cts +3 -3
- package/dist/oauth2/provider/apple-client-secret.d.mts +3 -3
- package/dist/oauth2/provider/apple-client-secret.mjs +3 -3
- package/dist/oauth2/provider/apple-client-secret.mjs.map +1 -1
- package/dist/oauth2/provider/apple.cjs +9 -3
- package/dist/oauth2/provider/apple.cjs.map +1 -1
- package/dist/oauth2/provider/apple.d.cts.map +1 -1
- package/dist/oauth2/provider/apple.d.mts.map +1 -1
- package/dist/oauth2/provider/apple.mjs +9 -3
- package/dist/oauth2/provider/apple.mjs.map +1 -1
- package/dist/oauth2/provider/common.cjs +13 -9
- package/dist/oauth2/provider/common.cjs.map +1 -1
- package/dist/oauth2/provider/common.d.cts +13 -2
- package/dist/oauth2/provider/common.d.cts.map +1 -1
- package/dist/oauth2/provider/common.d.mts +13 -2
- package/dist/oauth2/provider/common.d.mts.map +1 -1
- package/dist/oauth2/provider/common.mjs +13 -9
- package/dist/oauth2/provider/common.mjs.map +1 -1
- package/dist/oauth2/provider/facebook.cjs +59 -35
- package/dist/oauth2/provider/facebook.cjs.map +1 -1
- package/dist/oauth2/provider/facebook.d.cts.map +1 -1
- package/dist/oauth2/provider/facebook.d.mts.map +1 -1
- package/dist/oauth2/provider/facebook.mjs +59 -35
- package/dist/oauth2/provider/facebook.mjs.map +1 -1
- package/dist/oauth2/provider/google.cjs +31 -32
- package/dist/oauth2/provider/google.cjs.map +1 -1
- package/dist/oauth2/provider/google.d.cts.map +1 -1
- package/dist/oauth2/provider/google.d.mts.map +1 -1
- package/dist/oauth2/provider/google.mjs +31 -32
- package/dist/oauth2/provider/google.mjs.map +1 -1
- package/dist/oauth2/provider/index.cjs +1 -1
- package/dist/oauth2/provider/index.mjs +1 -1
- package/dist/oauth2/provider/wechat.cjs +1 -0
- package/dist/oauth2/provider/wechat.cjs.map +1 -1
- package/dist/oauth2/provider/wechat.d.cts.map +1 -1
- package/dist/oauth2/provider/wechat.d.mts.map +1 -1
- package/dist/oauth2/provider/wechat.mjs +1 -0
- package/dist/oauth2/provider/wechat.mjs.map +1 -1
- package/dist/oauth2/types.cjs.map +1 -1
- package/dist/oauth2/types.d.cts +69 -34
- package/dist/oauth2/types.d.cts.map +1 -1
- package/dist/oauth2/types.d.mts +69 -34
- package/dist/oauth2/types.d.mts.map +1 -1
- package/dist/oauth2/types.mjs.map +1 -1
- 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&
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
clientId: env.
|
|
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
|
-
|
|
58
|
-
provider:
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
clientSecret: env.
|
|
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
|
-
|
|
89
|
-
|
|
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 — `{
|
|
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
|
|
133
|
+
const PROVIDERS: Record<string, Provider | undefined> = {
|
|
134
|
+
apple: 'APPLE',
|
|
135
|
+
google: 'GOOGLE',
|
|
101
136
|
github: 'GITHUB',
|
|
102
|
-
|
|
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
|
-
{
|
|
142
|
+
{ provider, userInfo: { claims } }: Authorization
|
|
110
143
|
): Promise<Principal> {
|
|
111
|
-
const
|
|
112
|
-
if (!
|
|
113
|
-
const userId = await upsertUser(
|
|
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`, `
|
|
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
|
|
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
|
-
|
|
248
|
-
import { apple, createAppleClientSecret, github, google } from '@shware/security/oauth2/provider';
|
|
282
|
+
### Id token verification
|
|
249
283
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
286
|
-
|
|
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/:
|
|
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/:
|
|
293
|
-
so register both). `loginOAuth2Code` checks state, exchanges the code
|
|
294
|
-
and redirects to `successUri
|
|
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
|
-
|
|
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/:
|
|
335
|
-
(`{ id_token?, access_token?, code?, state?, client_id?, redirect_uri? }`)
|
|
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
|
|
340
|
-
|
|
341
|
-
|
|
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({
|
|
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
|
|
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
|
-
`
|
|
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 `:
|
|
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
|
|
392
|
-
| ------------------------------ |
|
|
393
|
-
| `CSRF` | `/csrf`
|
|
394
|
-
| `LOGOUT` | `/logout`
|
|
395
|
-
| `LOGGED` | `/logged`
|
|
396
|
-
| `OAUTH2_STATE` | `/oauth2/state/:
|
|
397
|
-
| `OAUTH2_NONCE` | `/oauth2/nonce/:
|
|
398
|
-
| `OAUTH2_AUTHORIZATION` | `/oauth2/authorization/:
|
|
399
|
-
| `LOGIN_OAUTH2_CODE` | `/login/oauth2/code/:
|
|
400
|
-
| `LOGIN_OAUTH2_NATIVE` | `/login/oauth2/native/:
|
|
401
|
-
| `LOGIN_OAUTH2_ONETAP` | `/login/oauth2/onetap/google`
|
|
402
|
-
| `LOGIN_EMAIL` | `/login/email`
|
|
403
|
-
| `LOGIN_PHONE` | `/login/phone`
|
|
404
|
-
| `SEND_EMAIL_VERIFICATION_CODE` | `/auth/sendEmailVerificationCode`
|
|
405
|
-
| `SEND_PHONE_VERIFICATION_CODE` | `/auth/sendPhoneVerificationCode`
|
|
406
|
-
| `DESKTOP_AUTHORIZE` | `/auth/desktop/authorize`
|
|
407
|
-
| `DESKTOP_EXCHANGE` | `/auth/desktop/exchange`
|
|
408
|
-
| `CLEANUP_EXPIRED_SESSIONS` | `/sessions/expired/cleanup`
|
|
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.
|
package/dist/auth/index.cjs
CHANGED
|
@@ -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/:
|
|
38
|
-
OAUTH2_NONCE: "/oauth2/nonce/:
|
|
39
|
-
OAUTH2_AUTHORIZATION: "/oauth2/authorization/:
|
|
40
|
-
LOGIN_OAUTH2_CODE: "/login/oauth2/code/:
|
|
41
|
-
LOGIN_OAUTH2_NATIVE: "/login/oauth2/native/:
|
|
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 {
|
|
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),
|
|
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 {
|
|
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),
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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,22 +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 (
|
|
221
|
+
if (provider !== cached.provider) return this.redirect("invalid_request", "redirect provider mismatch");
|
|
224
222
|
mark("validate_session");
|
|
225
223
|
const { code, user } = parsed.data;
|
|
226
|
-
const token = await this.oauth2Client.exchangeAuthorizationCode({
|
|
227
|
-
|
|
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
|
-
|
|
230
|
+
provider,
|
|
231
|
+
clientId,
|
|
233
232
|
token,
|
|
234
233
|
response: user ? { user } : void 0
|
|
235
234
|
});
|
|
236
235
|
mark("exchange_code");
|
|
237
236
|
const principal = await handler(request, {
|
|
238
|
-
|
|
237
|
+
provider,
|
|
238
|
+
clientId,
|
|
239
239
|
userInfo,
|
|
240
240
|
token,
|
|
241
241
|
attributes: cached.attributes ?? {}
|
|
@@ -248,7 +248,7 @@ var Auth = class {
|
|
|
248
248
|
mark("update_session");
|
|
249
249
|
const uri = new URL(this.oauth2Client.successUri);
|
|
250
250
|
uri.searchParams.set("status", "success");
|
|
251
|
-
uri.searchParams.set("
|
|
251
|
+
uri.searchParams.set("provider", provider);
|
|
252
252
|
const response = new Response(null, {
|
|
253
253
|
status: 302,
|
|
254
254
|
headers: { location: uri.href }
|
|
@@ -264,19 +264,19 @@ var Auth = class {
|
|
|
264
264
|
loginOAuth2Native = async (request, handler) => {
|
|
265
265
|
(0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
|
|
266
266
|
const { mark, setTiming } = require_utils_timing.timing({ enabled: this.timing });
|
|
267
|
-
const {
|
|
267
|
+
const { provider } = require_utils_http.param(request, PATH.LOGIN_OAUTH2_NATIVE);
|
|
268
268
|
const body = await request.json();
|
|
269
269
|
const attributes = require_auth_attributes.bodyAttributes(body);
|
|
270
270
|
if (!attributes) return Response.json(INVALID_ATTRIBUTES, { status: 400 });
|
|
271
271
|
const { attributes: _, ...credentials } = body;
|
|
272
|
-
const { userInfo, token } = await this.oauth2Client.loginOAuth2Native({
|
|
273
|
-
|
|
272
|
+
const { clientId, userInfo, token } = await this.oauth2Client.loginOAuth2Native({
|
|
273
|
+
provider,
|
|
274
274
|
credentials
|
|
275
275
|
});
|
|
276
276
|
mark("login_oauth2_native");
|
|
277
277
|
const { nonce } = userInfo.data;
|
|
278
278
|
if (nonce) {
|
|
279
|
-
if (await this.kv.getItem(this.getOauth2NonceKey(nonce)) !==
|
|
279
|
+
if (await this.kv.getItem(this.getOauth2NonceKey(nonce)) !== provider) return Response.json({
|
|
280
280
|
error: "invalid_request",
|
|
281
281
|
error_description: "nonce mismatch"
|
|
282
282
|
}, { status: 400 });
|
|
@@ -284,7 +284,8 @@ var Auth = class {
|
|
|
284
284
|
mark("check_nonce");
|
|
285
285
|
}
|
|
286
286
|
const principal = await handler(request, {
|
|
287
|
-
|
|
287
|
+
provider,
|
|
288
|
+
clientId,
|
|
288
289
|
userInfo,
|
|
289
290
|
token,
|
|
290
291
|
attributes
|
|
@@ -305,7 +306,7 @@ var Auth = class {
|
|
|
305
306
|
setTiming(response);
|
|
306
307
|
return response;
|
|
307
308
|
};
|
|
308
|
-
loginOAuth2Onetap = async (request, handler,
|
|
309
|
+
loginOAuth2Onetap = async (request, handler, provider = "google") => {
|
|
309
310
|
(0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
|
|
310
311
|
const { mark, setTiming } = require_utils_timing.timing({ enabled: this.timing });
|
|
311
312
|
let body;
|
|
@@ -325,22 +326,28 @@ var Auth = class {
|
|
|
325
326
|
const parsed = require_oauth2_client.googleOneTapSchema.parse(body);
|
|
326
327
|
let principal;
|
|
327
328
|
if (parsed.credential) {
|
|
328
|
-
const { token, userInfo } = await
|
|
329
|
+
const { clientId, token, userInfo } = await this.oauth2Client.getTokenInfo({
|
|
330
|
+
provider,
|
|
331
|
+
credential: parsed.credential
|
|
332
|
+
});
|
|
329
333
|
principal = await handler(request, {
|
|
330
|
-
|
|
334
|
+
provider,
|
|
335
|
+
clientId,
|
|
331
336
|
userInfo,
|
|
332
337
|
token,
|
|
333
338
|
attributes
|
|
334
339
|
});
|
|
335
340
|
} else if (parsed.code) {
|
|
336
|
-
const token = await this.oauth2Client.exchangeAuthorizationCode({
|
|
337
|
-
|
|
341
|
+
const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({
|
|
342
|
+
provider,
|
|
338
343
|
code: parsed.code
|
|
339
344
|
});
|
|
340
345
|
principal = await handler(request, {
|
|
341
|
-
|
|
346
|
+
provider,
|
|
347
|
+
clientId,
|
|
342
348
|
userInfo: await this.oauth2Client.getUserInfo({
|
|
343
|
-
|
|
349
|
+
provider,
|
|
350
|
+
clientId,
|
|
344
351
|
token
|
|
345
352
|
}),
|
|
346
353
|
token,
|
|
@@ -392,7 +399,7 @@ var Auth = class {
|
|
|
392
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 });
|
|
393
400
|
await this.kv.removeItem(key);
|
|
394
401
|
const principal = await handler(request, {
|
|
395
|
-
|
|
402
|
+
provider: "EMAIL",
|
|
396
403
|
userInfo: {
|
|
397
404
|
claims: {
|
|
398
405
|
sub: email,
|