@atproto/oauth-provider 0.16.3 → 0.17.0-next.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/CHANGELOG.md +42 -0
- package/dist/access-token/access-token-mode.js +2 -5
- package/dist/access-token/access-token-mode.js.map +1 -1
- package/dist/account/account-manager.js +25 -33
- package/dist/account/account-manager.js.map +1 -1
- package/dist/account/account-store.js +11 -32
- package/dist/account/account-store.js.map +1 -1
- package/dist/account/sign-in-data.js +9 -12
- package/dist/account/sign-in-data.js.map +1 -1
- package/dist/account/sign-up-input.js +14 -17
- package/dist/account/sign-up-input.js.map +1 -1
- package/dist/client/client-auth.js +1 -2
- package/dist/client/client-data.js +1 -2
- package/dist/client/client-id.js +2 -5
- package/dist/client/client-id.js.map +1 -1
- package/dist/client/client-info.js +1 -2
- package/dist/client/client-manager.js +86 -97
- package/dist/client/client-manager.js.map +1 -1
- package/dist/client/client-store.js +7 -26
- package/dist/client/client-store.js.map +1 -1
- package/dist/client/client-utils.js +10 -14
- package/dist/client/client-utils.js.map +1 -1
- package/dist/client/client.js +43 -53
- package/dist/client/client.js.map +1 -1
- package/dist/constants.js +28 -31
- package/dist/constants.js.map +1 -1
- package/dist/customization/branding.js +8 -11
- package/dist/customization/branding.js.map +1 -1
- package/dist/customization/build-customization-css.js +8 -11
- package/dist/customization/build-customization-css.js.map +1 -1
- package/dist/customization/build-customization-data.js +1 -4
- package/dist/customization/build-customization-data.js.map +1 -1
- package/dist/customization/colors.js +11 -14
- package/dist/customization/colors.js.map +1 -1
- package/dist/customization/customization.js +8 -11
- package/dist/customization/customization.js.map +1 -1
- package/dist/customization/links.js +7 -10
- package/dist/customization/links.js.map +1 -1
- package/dist/device/device-data.js +7 -10
- package/dist/device/device-data.js.map +1 -1
- package/dist/device/device-id.js +11 -16
- package/dist/device/device-id.js.map +1 -1
- package/dist/device/device-manager.js +32 -38
- package/dist/device/device-manager.js.map +1 -1
- package/dist/device/device-store.js +7 -25
- package/dist/device/device-store.js.map +1 -1
- package/dist/device/session-id.js +9 -13
- package/dist/device/session-id.js.map +1 -1
- package/dist/dpop/dpop-manager.d.ts +3 -3
- package/dist/dpop/dpop-manager.js +38 -43
- package/dist/dpop/dpop-manager.js.map +1 -1
- package/dist/dpop/dpop-nonce.d.ts +2 -2
- package/dist/dpop/dpop-nonce.d.ts.map +1 -1
- package/dist/dpop/dpop-nonce.js +14 -18
- package/dist/dpop/dpop-nonce.js.map +1 -1
- package/dist/dpop/dpop-proof.js +1 -2
- package/dist/errors/access-denied-error.js +2 -6
- package/dist/errors/access-denied-error.js.map +1 -1
- package/dist/errors/account-selection-required-error.js +2 -6
- package/dist/errors/account-selection-required-error.js.map +1 -1
- package/dist/errors/authorization-error.js +7 -12
- package/dist/errors/authorization-error.js.map +1 -1
- package/dist/errors/consent-required-error.js +2 -6
- package/dist/errors/consent-required-error.js.map +1 -1
- package/dist/errors/error-parser.js +14 -18
- package/dist/errors/error-parser.js.map +1 -1
- package/dist/errors/handle-unavailable-error.js +2 -7
- package/dist/errors/handle-unavailable-error.js.map +1 -1
- package/dist/errors/invalid-authorization-details-error.js +2 -6
- package/dist/errors/invalid-authorization-details-error.js.map +1 -1
- package/dist/errors/invalid-client-error.js +2 -6
- package/dist/errors/invalid-client-error.js.map +1 -1
- package/dist/errors/invalid-client-id-error.js +2 -6
- package/dist/errors/invalid-client-id-error.js.map +1 -1
- package/dist/errors/invalid-client-metadata-error.js +7 -11
- package/dist/errors/invalid-client-metadata-error.js.map +1 -1
- package/dist/errors/invalid-credentials-error.js +2 -7
- package/dist/errors/invalid-credentials-error.js.map +1 -1
- package/dist/errors/invalid-dpop-key-binding-error.js +2 -6
- package/dist/errors/invalid-dpop-key-binding-error.js.map +1 -1
- package/dist/errors/invalid-dpop-proof-error.js +2 -6
- package/dist/errors/invalid-dpop-proof-error.js.map +1 -1
- package/dist/errors/invalid-grant-error.js +2 -6
- package/dist/errors/invalid-grant-error.js.map +1 -1
- package/dist/errors/invalid-invite-code-error.d.ts +1 -1
- package/dist/errors/invalid-invite-code-error.d.ts.map +1 -1
- package/dist/errors/invalid-invite-code-error.js +2 -6
- package/dist/errors/invalid-invite-code-error.js.map +1 -1
- package/dist/errors/invalid-redirect-uri-error.js +2 -6
- package/dist/errors/invalid-redirect-uri-error.js.map +1 -1
- package/dist/errors/invalid-request-error.js +3 -7
- package/dist/errors/invalid-request-error.js.map +1 -1
- package/dist/errors/invalid-scope-error.js +2 -6
- package/dist/errors/invalid-scope-error.js.map +1 -1
- package/dist/errors/invalid-token-error.js +10 -15
- package/dist/errors/invalid-token-error.js.map +1 -1
- package/dist/errors/login-required-error.js +2 -6
- package/dist/errors/login-required-error.js.map +1 -1
- package/dist/errors/oauth-error.js +1 -9
- package/dist/errors/oauth-error.js.map +1 -1
- package/dist/errors/second-authentication-factor-required-error.js +2 -8
- package/dist/errors/second-authentication-factor-required-error.js.map +1 -1
- package/dist/errors/unauthorized-client-error.js +2 -6
- package/dist/errors/unauthorized-client-error.js.map +1 -1
- package/dist/errors/use-dpop-nonce-error.js +4 -8
- package/dist/errors/use-dpop-nonce-error.js.map +1 -1
- package/dist/errors/www-authenticate-error.js +4 -9
- package/dist/errors/www-authenticate-error.js.map +1 -1
- package/dist/index.js +14 -30
- package/dist/index.js.map +1 -1
- package/dist/lexicon/lexicon-data.js +1 -2
- package/dist/lexicon/lexicon-getter.js +6 -10
- package/dist/lexicon/lexicon-getter.js.map +1 -1
- package/dist/lexicon/lexicon-manager.js +10 -30
- package/dist/lexicon/lexicon-manager.js.map +1 -1
- package/dist/lexicon/lexicon-store.js +5 -10
- package/dist/lexicon/lexicon-store.js.map +1 -1
- package/dist/lib/csp/index.js +3 -8
- package/dist/lib/csp/index.js.map +1 -1
- package/dist/lib/hcaptcha.js +33 -43
- package/dist/lib/hcaptcha.js.map +1 -1
- package/dist/lib/html/build-document.js +19 -24
- package/dist/lib/html/build-document.js.map +1 -1
- package/dist/lib/html/escapers.js +10 -16
- package/dist/lib/html/escapers.js.map +1 -1
- package/dist/lib/html/html.js +1 -5
- package/dist/lib/html/html.js.map +1 -1
- package/dist/lib/html/hydration-data.js +6 -10
- package/dist/lib/html/hydration-data.js.map +1 -1
- package/dist/lib/html/index.js +3 -19
- package/dist/lib/html/index.js.map +1 -1
- package/dist/lib/html/tags.js +14 -23
- package/dist/lib/html/tags.js.map +1 -1
- package/dist/lib/html/util.js +1 -4
- package/dist/lib/html/util.js.map +1 -1
- package/dist/lib/http/accept.d.ts.map +1 -1
- package/dist/lib/http/accept.js +8 -8
- package/dist/lib/http/accept.js.map +1 -1
- package/dist/lib/http/context.js +1 -4
- package/dist/lib/http/context.js.map +1 -1
- package/dist/lib/http/headers.js +1 -4
- package/dist/lib/http/headers.js.map +1 -1
- package/dist/lib/http/index.js +10 -26
- package/dist/lib/http/index.js.map +1 -1
- package/dist/lib/http/method.js +1 -4
- package/dist/lib/http/method.js.map +1 -1
- package/dist/lib/http/middleware.js +11 -17
- package/dist/lib/http/middleware.js.map +1 -1
- package/dist/lib/http/parser.js +13 -20
- package/dist/lib/http/parser.js.map +1 -1
- package/dist/lib/http/path.js +1 -4
- package/dist/lib/http/path.js.map +1 -1
- package/dist/lib/http/request.d.ts.map +1 -1
- package/dist/lib/http/request.js +32 -47
- package/dist/lib/http/request.js.map +1 -1
- package/dist/lib/http/response.js +14 -27
- package/dist/lib/http/response.js.map +1 -1
- package/dist/lib/http/route.js +9 -12
- package/dist/lib/http/route.js.map +1 -1
- package/dist/lib/http/router.js +8 -13
- package/dist/lib/http/router.js.map +1 -1
- package/dist/lib/http/security-headers.js +10 -15
- package/dist/lib/http/security-headers.js.map +1 -1
- package/dist/lib/http/stream.js +12 -20
- package/dist/lib/http/stream.js.map +1 -1
- package/dist/lib/http/types.js +1 -2
- package/dist/lib/http/url.js +1 -4
- package/dist/lib/http/url.js.map +1 -1
- package/dist/lib/nsid.js +4 -8
- package/dist/lib/nsid.js.map +1 -1
- package/dist/lib/redis.js +4 -7
- package/dist/lib/redis.js.map +1 -1
- package/dist/lib/util/authorization-header.js +11 -15
- package/dist/lib/util/authorization-header.js.map +1 -1
- package/dist/lib/util/cast.js +3 -8
- package/dist/lib/util/cast.js.map +1 -1
- package/dist/lib/util/color.js +23 -32
- package/dist/lib/util/color.js.map +1 -1
- package/dist/lib/util/crypto.js +5 -10
- package/dist/lib/util/crypto.js.map +1 -1
- package/dist/lib/util/date.js +2 -6
- package/dist/lib/util/date.js.map +1 -1
- package/dist/lib/util/error.js +5 -8
- package/dist/lib/util/error.js.map +1 -1
- package/dist/lib/util/function.js +3 -8
- package/dist/lib/util/function.js.map +1 -1
- package/dist/lib/util/locale.js +3 -6
- package/dist/lib/util/locale.js.map +1 -1
- package/dist/lib/util/object.js +1 -4
- package/dist/lib/util/object.js.map +1 -1
- package/dist/lib/util/redirect-uri.js +3 -6
- package/dist/lib/util/redirect-uri.js.map +1 -1
- package/dist/lib/util/time.js +5 -9
- package/dist/lib/util/time.js.map +1 -1
- package/dist/lib/util/type.d.ts.map +1 -1
- package/dist/lib/util/type.js +1 -5
- package/dist/lib/util/type.js.map +1 -1
- package/dist/lib/util/ui8.js +3 -8
- package/dist/lib/util/ui8.js.map +1 -1
- package/dist/lib/util/well-known.js +1 -4
- package/dist/lib/util/well-known.js.map +1 -1
- package/dist/lib/util/zod-error.js +4 -8
- package/dist/lib/util/zod-error.js.map +1 -1
- package/dist/lib/write-form-redirect.js +9 -12
- package/dist/lib/write-form-redirect.js.map +1 -1
- package/dist/lib/write-html.js +12 -15
- package/dist/lib/write-html.js.map +1 -1
- package/dist/metadata/build-metadata.js +9 -12
- package/dist/metadata/build-metadata.js.map +1 -1
- package/dist/oauth-client.js +2 -18
- package/dist/oauth-client.js.map +1 -1
- package/dist/oauth-dpop.js +2 -18
- package/dist/oauth-dpop.js.map +1 -1
- package/dist/oauth-errors.js +24 -42
- package/dist/oauth-errors.js.map +1 -1
- package/dist/oauth-hooks.js +8 -15
- package/dist/oauth-hooks.js.map +1 -1
- package/dist/oauth-middleware.js +13 -16
- package/dist/oauth-middleware.js.map +1 -1
- package/dist/oauth-provider.js +108 -125
- package/dist/oauth-provider.js.map +1 -1
- package/dist/oauth-store.js +7 -23
- package/dist/oauth-store.js.map +1 -1
- package/dist/oauth-verifier.js +41 -53
- package/dist/oauth-verifier.js.map +1 -1
- package/dist/oidc/sub.js +2 -5
- package/dist/oidc/sub.js.map +1 -1
- package/dist/replay/replay-manager.js +6 -11
- package/dist/replay/replay-manager.js.map +1 -1
- package/dist/replay/replay-store-memory.js +5 -7
- package/dist/replay/replay-store-memory.js.map +1 -1
- package/dist/replay/replay-store-redis.js +3 -8
- package/dist/replay/replay-store-redis.js.map +1 -1
- package/dist/replay/replay-store.js +3 -8
- package/dist/replay/replay-store.js.map +1 -1
- package/dist/request/code.js +10 -15
- package/dist/request/code.js.map +1 -1
- package/dist/request/request-data.js +1 -5
- package/dist/request/request-data.js.map +1 -1
- package/dist/request/request-id.js +9 -13
- package/dist/request/request-id.js.map +1 -1
- package/dist/request/request-manager.js +61 -71
- package/dist/request/request-manager.js.map +1 -1
- package/dist/request/request-store.js +9 -27
- package/dist/request/request-store.js.map +1 -1
- package/dist/request/request-uri.js +17 -23
- package/dist/request/request-uri.js.map +1 -1
- package/dist/result/authorization-redirect-parameters.js +1 -2
- package/dist/result/authorization-result-authorize-page.js +1 -2
- package/dist/result/authorization-result-redirect.js +1 -2
- package/dist/router/assets/assets-manifest.d.ts.map +1 -1
- package/dist/router/assets/assets-manifest.js +14 -15
- package/dist/router/assets/assets-manifest.js.map +1 -1
- package/dist/router/assets/assets.d.ts.map +1 -1
- package/dist/router/assets/assets.js +25 -27
- package/dist/router/assets/assets.js.map +1 -1
- package/dist/router/assets/csrf.js +16 -25
- package/dist/router/assets/csrf.js.map +1 -1
- package/dist/router/assets/send-account-page.js +3 -6
- package/dist/router/assets/send-account-page.js.map +1 -1
- package/dist/router/assets/send-authorization-page.js +3 -6
- package/dist/router/assets/send-authorization-page.js.map +1 -1
- package/dist/router/assets/send-cookie-error-page.js +3 -6
- package/dist/router/assets/send-cookie-error-page.js.map +1 -1
- package/dist/router/assets/send-error-page.js +6 -9
- package/dist/router/assets/send-error-page.js.map +1 -1
- package/dist/router/assets/send-redirect.js +12 -20
- package/dist/router/assets/send-redirect.js.map +1 -1
- package/dist/router/create-account-page-middleware.js +11 -14
- package/dist/router/create-account-page-middleware.js.map +1 -1
- package/dist/router/create-api-middleware.js +83 -90
- package/dist/router/create-api-middleware.js.map +1 -1
- package/dist/router/create-authorization-page-middleware.js +43 -46
- package/dist/router/create-authorization-page-middleware.js.map +1 -1
- package/dist/router/create-oauth-middleware.js +31 -34
- package/dist/router/create-oauth-middleware.js.map +1 -1
- package/dist/router/error-handler.js +1 -2
- package/dist/router/middleware-options.js +1 -2
- package/dist/signer/access-token-payload.js +12 -15
- package/dist/signer/access-token-payload.js.map +1 -1
- package/dist/signer/api-token-payload.js +8 -11
- package/dist/signer/api-token-payload.js.map +1 -1
- package/dist/signer/signer.js +11 -17
- package/dist/signer/signer.js.map +1 -1
- package/dist/token/refresh-token.js +10 -15
- package/dist/token/refresh-token.js.map +1 -1
- package/dist/token/token-claims.js +1 -2
- package/dist/token/token-data.js +1 -2
- package/dist/token/token-id.js +10 -15
- package/dist/token/token-id.js.map +1 -1
- package/dist/token/token-manager.js +40 -51
- package/dist/token/token-manager.js.map +1 -1
- package/dist/token/token-store.js +7 -25
- package/dist/token/token-store.js.map +1 -1
- package/dist/types/authorization-response-error.js +8 -12
- package/dist/types/authorization-response-error.js.map +1 -1
- package/dist/types/color-hue.js +2 -5
- package/dist/types/color-hue.js.map +1 -1
- package/dist/types/email-otp.js +2 -5
- package/dist/types/email-otp.js.map +1 -1
- package/dist/types/email.js +6 -9
- package/dist/types/email.js.map +1 -1
- package/dist/types/handle.js +6 -9
- package/dist/types/handle.js.map +1 -1
- package/dist/types/invite-code.js +2 -5
- package/dist/types/invite-code.js.map +1 -1
- package/dist/types/par-response-error.js +5 -9
- package/dist/types/par-response-error.js.map +1 -1
- package/dist/types/password.js +3 -6
- package/dist/types/password.js.map +1 -1
- package/dist/types/rgb-color.js +7 -10
- package/dist/types/rgb-color.js.map +1 -1
- package/package.json +20 -22
- package/src/dpop/dpop-nonce.ts +1 -1
- package/src/errors/invalid-invite-code-error.ts +1 -1
- package/src/lib/http/accept.ts +4 -1
- package/src/lib/http/request.ts +4 -1
- package/src/lib/util/type.ts +0 -1
- package/src/router/assets/assets-manifest.ts +3 -1
- package/src/router/assets/assets.ts +2 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1,42 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const request_js_1 = require("../lib/http/request.js");
|
|
8
|
-
const device_id_js_1 = require("./device-id.js");
|
|
9
|
-
const session_id_js_1 = require("./session-id.js");
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { SESSION_FIXATION_MAX_AGE } from '../constants.js';
|
|
3
|
+
import { parseHttpCookies } from '../lib/http/index.js';
|
|
4
|
+
import { extractRequestMetadata, setCookie, } from '../lib/http/request.js';
|
|
5
|
+
import { deviceIdSchema, generateDeviceId } from './device-id.js';
|
|
6
|
+
import { generateSessionId, sessionIdSchema } from './session-id.js';
|
|
10
7
|
/**
|
|
11
8
|
* @see {@link https://www.npmjs.com/package/keygrip | Keygrip}
|
|
12
9
|
*/
|
|
13
|
-
|
|
14
|
-
sign:
|
|
15
|
-
verify:
|
|
16
|
-
index:
|
|
10
|
+
export const keygripSchema = z.object({
|
|
11
|
+
sign: z.function().args(z.any()).returns(z.string()),
|
|
12
|
+
verify: z.function().args(z.any(), z.string()).returns(z.boolean()),
|
|
13
|
+
index: z.function().args(z.any(), z.string()).returns(z.number()),
|
|
17
14
|
});
|
|
18
|
-
|
|
15
|
+
export const deviceManagerOptionsSchema = z.object({
|
|
19
16
|
/**
|
|
20
17
|
* Controls whether the IP address is read from the `X-Forwarded-For` header
|
|
21
18
|
* (if `true`), or from the `req.socket.remoteAddress` property (if `false`).
|
|
22
19
|
*/
|
|
23
|
-
trustProxy:
|
|
20
|
+
trustProxy: z
|
|
24
21
|
.function()
|
|
25
|
-
.args(
|
|
26
|
-
.returns(
|
|
22
|
+
.args(z.string(), z.number())
|
|
23
|
+
.returns(z.boolean())
|
|
27
24
|
.optional(),
|
|
28
25
|
/**
|
|
29
26
|
* Amount of time (in ms) after which session IDs will be rotated
|
|
30
27
|
*
|
|
31
28
|
* @default 300e3 // (5 minutes)
|
|
32
29
|
*/
|
|
33
|
-
rotationRate:
|
|
30
|
+
rotationRate: z.number().default(300e3),
|
|
34
31
|
/**
|
|
35
32
|
* Cookie options
|
|
36
33
|
*/
|
|
37
|
-
cookie:
|
|
34
|
+
cookie: z
|
|
38
35
|
.object({
|
|
39
|
-
keys:
|
|
36
|
+
keys: keygripSchema.optional(),
|
|
40
37
|
/**
|
|
41
38
|
* Amount of time (in ms) after which the session cookie will expire.
|
|
42
39
|
* If set to `null`, the cookie will be a session cookie (deleted when the
|
|
@@ -44,7 +41,7 @@ exports.deviceManagerOptionsSchema = zod_1.z.object({
|
|
|
44
41
|
*
|
|
45
42
|
* @default 10 years
|
|
46
43
|
*/
|
|
47
|
-
age:
|
|
44
|
+
age: z
|
|
48
45
|
.number()
|
|
49
46
|
.nullable()
|
|
50
47
|
.default(10 * 365.2 * 24 * 60 * 60e3),
|
|
@@ -53,13 +50,13 @@ exports.deviceManagerOptionsSchema = zod_1.z.object({
|
|
|
53
50
|
* over HTTP (if `false`). This should **NOT** be set to `false` in
|
|
54
51
|
* production.
|
|
55
52
|
*/
|
|
56
|
-
secure:
|
|
53
|
+
secure: z.boolean().default(true),
|
|
57
54
|
/**
|
|
58
55
|
* Controls whether the cookie is sent along with cross-site requests.
|
|
59
56
|
*
|
|
60
57
|
* @default 'lax'
|
|
61
58
|
*/
|
|
62
|
-
sameSite:
|
|
59
|
+
sameSite: z.enum(['lax', 'strict']).default('lax'),
|
|
63
60
|
})
|
|
64
61
|
.default({}),
|
|
65
62
|
});
|
|
@@ -68,12 +65,10 @@ exports.deviceManagerOptionsSchema = zod_1.z.object({
|
|
|
68
65
|
* relies on a {@link DeviceStore} to persist session data and a cookie to
|
|
69
66
|
* identify the session.
|
|
70
67
|
*/
|
|
71
|
-
class DeviceManager {
|
|
72
|
-
store;
|
|
73
|
-
options;
|
|
68
|
+
export class DeviceManager {
|
|
74
69
|
constructor(store, options = {}) {
|
|
75
70
|
this.store = store;
|
|
76
|
-
this.options =
|
|
71
|
+
this.options = deviceManagerOptionsSchema.parse(options);
|
|
77
72
|
}
|
|
78
73
|
async hasSession(req) {
|
|
79
74
|
const cookies = await this.getCookies(req);
|
|
@@ -91,8 +86,8 @@ class DeviceManager {
|
|
|
91
86
|
async create(req, res) {
|
|
92
87
|
const deviceMetadata = this.getRequestMetadata(req);
|
|
93
88
|
const [deviceId, sessionId] = await Promise.all([
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
generateDeviceId(),
|
|
90
|
+
generateSessionId(),
|
|
96
91
|
]);
|
|
97
92
|
await this.store.createDevice(deviceId, {
|
|
98
93
|
sessionId,
|
|
@@ -110,7 +105,7 @@ class DeviceManager {
|
|
|
110
105
|
const lastSeenAt = new Date(data.lastSeenAt);
|
|
111
106
|
const age = Date.now() - lastSeenAt.getTime();
|
|
112
107
|
if (sessionId !== data.sessionId) {
|
|
113
|
-
if (age <=
|
|
108
|
+
if (age <= SESSION_FIXATION_MAX_AGE) {
|
|
114
109
|
// The cookie was probably rotated by a concurrent request. Let's
|
|
115
110
|
// update the cookie with the new sessionId.
|
|
116
111
|
forceRotate = true;
|
|
@@ -135,7 +130,7 @@ class DeviceManager {
|
|
|
135
130
|
return { deviceId, deviceMetadata };
|
|
136
131
|
}
|
|
137
132
|
async rotate(req, res, deviceId, data) {
|
|
138
|
-
const sessionId = await
|
|
133
|
+
const sessionId = await generateSessionId();
|
|
139
134
|
await this.store.updateDevice(deviceId, {
|
|
140
135
|
...data,
|
|
141
136
|
sessionId,
|
|
@@ -144,9 +139,9 @@ class DeviceManager {
|
|
|
144
139
|
await this.setCookies(req, res, { deviceId, sessionId });
|
|
145
140
|
}
|
|
146
141
|
async getCookies(req) {
|
|
147
|
-
const cookies =
|
|
148
|
-
const device = this.parseCookie(cookies, `dev-id`,
|
|
149
|
-
const session = this.parseCookie(cookies, `ses-id`,
|
|
142
|
+
const cookies = parseHttpCookies(req);
|
|
143
|
+
const device = this.parseCookie(cookies, `dev-id`, deviceIdSchema);
|
|
144
|
+
const session = this.parseCookie(cookies, `ses-id`, sessionIdSchema);
|
|
150
145
|
const deviceId = device?.value;
|
|
151
146
|
const sessionId = session?.value;
|
|
152
147
|
// Silently ignore invalid cookies
|
|
@@ -197,15 +192,14 @@ class DeviceManager {
|
|
|
197
192
|
secure: this.options.cookie.secure !== false,
|
|
198
193
|
sameSite: this.options.cookie.sameSite,
|
|
199
194
|
};
|
|
200
|
-
|
|
195
|
+
setCookie(res, name, value || '', cookieOptions);
|
|
201
196
|
if (this.options.cookie.keys) {
|
|
202
197
|
const hash = value ? this.options.cookie.keys.sign(value) : '';
|
|
203
|
-
|
|
198
|
+
setCookie(res, `${name}:hash`, hash, cookieOptions);
|
|
204
199
|
}
|
|
205
200
|
}
|
|
206
201
|
getRequestMetadata(req) {
|
|
207
|
-
return
|
|
202
|
+
return extractRequestMetadata(req, this.options);
|
|
208
203
|
}
|
|
209
204
|
}
|
|
210
|
-
exports.DeviceManager = DeviceManager;
|
|
211
205
|
//# sourceMappingURL=device-manager.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device-manager.js","sourceRoot":"","sources":["../../src/device/device-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { z } from 'zod'\nimport { SESSION_FIXATION_MAX_AGE } from '../constants.js'\nimport { parseHttpCookies } from '../lib/http/index.js'\nimport {\n RequestMetadata,\n extractRequestMetadata,\n setCookie,\n} from '../lib/http/request.js'\nimport { DeviceData } from './device-data.js'\nimport { DeviceId, deviceIdSchema, generateDeviceId } from './device-id.js'\nimport { DeviceStore } from './device-store.js'\nimport { generateSessionId, sessionIdSchema } from './session-id.js'\n\n/**\n * @see {@link https://www.npmjs.com/package/keygrip | Keygrip}\n */\nexport const keygripSchema = z.object({\n sign: z.function().args(z.any()).returns(z.string()),\n verify: z.function().args(z.any(), z.string()).returns(z.boolean()),\n index: z.function().args(z.any(), z.string()).returns(z.number()),\n})\n\nexport const deviceManagerOptionsSchema = z.object({\n /**\n * Controls whether the IP address is read from the `X-Forwarded-For` header\n * (if `true`), or from the `req.socket.remoteAddress` property (if `false`).\n */\n trustProxy: z\n .function()\n .args<[addr: z.ZodString, i: z.ZodNumber]>(z.string(), z.number())\n .returns(z.boolean())\n .optional(),\n\n /**\n * Amount of time (in ms) after which session IDs will be rotated\n *\n * @default 300e3 // (5 minutes)\n */\n rotationRate: z.number().default(300e3),\n /**\n * Cookie options\n */\n cookie: z\n .object({\n keys: keygripSchema.optional(),\n /**\n * Amount of time (in ms) after which the session cookie will expire.\n * If set to `null`, the cookie will be a session cookie (deleted when the\n * browser is closed).\n *\n * @default 10 years\n */\n age: z\n .number()\n .nullable()\n .default(10 * 365.2 * 24 * 60 * 60e3),\n /**\n * Controls whether the cookie is only sent over HTTPS (if `true`), or also\n * over HTTP (if `false`). This should **NOT** be set to `false` in\n * production.\n */\n secure: z.boolean().default(true),\n /**\n * Controls whether the cookie is sent along with cross-site requests.\n *\n * @default 'lax'\n */\n sameSite: z.enum(['lax', 'strict']).default('lax'),\n })\n .default({}),\n})\n\nexport type DeviceManagerOptions = z.input<typeof deviceManagerOptionsSchema>\n\ntype CookieValue = {\n deviceId: DeviceId\n sessionId: string\n}\n\nexport type DeviceInfo = {\n deviceId: DeviceId\n deviceMetadata: RequestMetadata\n}\n\n/**\n * This class provides an abstraction for keeping track of DEVICE sessions. It\n * relies on a {@link DeviceStore} to persist session data and a cookie to\n * identify the session.\n */\nexport class DeviceManager {\n private readonly options: z.output<typeof deviceManagerOptionsSchema>\n\n constructor(\n private readonly store: DeviceStore,\n options: DeviceManagerOptions = {},\n ) {\n this.options = deviceManagerOptionsSchema.parse(options)\n }\n\n public async hasSession(req: IncomingMessage): Promise<boolean> {\n const cookies = await this.getCookies(req)\n return cookies !== null\n }\n\n public async load(\n req: IncomingMessage,\n res: ServerResponse,\n forceRotate = false,\n ): Promise<DeviceInfo> {\n const cookie = await this.getCookies(req)\n if (cookie) {\n return this.refresh(\n req,\n res,\n cookie.value,\n forceRotate || cookie.mustRotate,\n )\n } else {\n return this.create(req, res)\n }\n }\n\n private async create(\n req: IncomingMessage,\n res: ServerResponse,\n ): Promise<DeviceInfo> {\n const deviceMetadata = this.getRequestMetadata(req)\n\n const [deviceId, sessionId] = await Promise.all([\n generateDeviceId(),\n generateSessionId(),\n ] as const)\n\n await this.store.createDevice(deviceId, {\n sessionId,\n lastSeenAt: new Date(),\n userAgent: deviceMetadata.userAgent ?? null,\n ipAddress: deviceMetadata.ipAddress,\n })\n\n await this.setCookies(req, res, { deviceId, sessionId })\n\n return { deviceId, deviceMetadata }\n }\n\n private async refresh(\n req: IncomingMessage,\n res: ServerResponse,\n { deviceId, sessionId }: CookieValue,\n forceRotate = false,\n ): Promise<DeviceInfo> {\n const data = await this.store.readDevice(deviceId)\n if (!data) return this.create(req, res)\n\n const lastSeenAt = new Date(data.lastSeenAt)\n const age = Date.now() - lastSeenAt.getTime()\n\n if (sessionId !== data.sessionId) {\n if (age <= SESSION_FIXATION_MAX_AGE) {\n // The cookie was probably rotated by a concurrent request. Let's\n // update the cookie with the new sessionId.\n forceRotate = true\n } else {\n // Something's wrong. Let's create a new session.\n await this.store.deleteDevice(deviceId)\n return this.create(req, res)\n }\n }\n\n const deviceMetadata = this.getRequestMetadata(req)\n\n const shouldRotate =\n forceRotate ||\n deviceMetadata.ipAddress !== data.ipAddress ||\n deviceMetadata.userAgent !== data.userAgent ||\n age > this.options.rotationRate\n\n if (shouldRotate) {\n await this.rotate(req, res, deviceId, {\n ipAddress: deviceMetadata.ipAddress,\n userAgent: deviceMetadata.userAgent || data.userAgent,\n })\n }\n\n return { deviceId, deviceMetadata }\n }\n\n private async rotate(\n req: IncomingMessage,\n res: ServerResponse,\n deviceId: DeviceId,\n data?: Partial<Omit<DeviceData, 'sessionId' | 'lastSeenAt'>>,\n ): Promise<void> {\n const sessionId = await generateSessionId()\n\n await this.store.updateDevice(deviceId, {\n ...data,\n sessionId,\n lastSeenAt: new Date(),\n })\n\n await this.setCookies(req, res, { deviceId, sessionId })\n }\n\n private async getCookies(\n req: IncomingMessage,\n ): Promise<{ value: CookieValue; mustRotate: boolean } | null> {\n const cookies = parseHttpCookies(req)\n\n const device = this.parseCookie(cookies, `dev-id`, deviceIdSchema)\n const session = this.parseCookie(cookies, `ses-id`, sessionIdSchema)\n\n const deviceId = device?.value\n const sessionId = session?.value\n\n // Silently ignore invalid cookies\n if (!deviceId || !sessionId) {\n // If the device cookie is still present, let's cleanup the DB\n if (deviceId) await this.store.deleteDevice(deviceId)\n\n return null\n }\n\n return {\n value: { deviceId, sessionId },\n mustRotate: device.mustRotate || session.mustRotate,\n }\n }\n\n private parseCookie<T>(\n cookies: Record<string, string | undefined>,\n name: string,\n schema: z.ZodType<T> | z.ZodEffects<z.ZodTypeAny, T, string>,\n ): null | { value: T; mustRotate: boolean } {\n const rawValue = Object.hasOwn(cookies, name) ? cookies[name] : null\n if (!rawValue) return null\n\n const result = schema.safeParse(rawValue)\n if (!result.success) return null\n\n const value = result.data\n\n if (this.options.cookie.keys) {\n const hashName = `${name}:hash`\n\n const hash = Object.hasOwn(cookies, hashName) ? cookies[hashName] : null\n if (!hash) return null\n\n const idx = this.options.cookie.keys.index(rawValue, hash)\n if (idx < 0) return null\n\n return { value, mustRotate: idx !== 0 }\n }\n\n return { value, mustRotate: false }\n }\n\n private async setCookies(\n req: IncomingMessage,\n res: ServerResponse,\n { deviceId, sessionId }: CookieValue,\n ) {\n this.writeCookie(res, `dev-id`, deviceId)\n this.writeCookie(res, `ses-id`, sessionId)\n }\n\n private writeCookie(res: ServerResponse, name: string, value?: string) {\n const cookieOptions = {\n maxAge: value\n ? this.options.cookie.age == null\n ? undefined\n : this.options.cookie.age / 1000\n : 0,\n httpOnly: true,\n path: '/',\n secure: this.options.cookie.secure !== false,\n sameSite: this.options.cookie.sameSite,\n } as const\n\n setCookie(res, name, value || '', cookieOptions)\n\n if (this.options.cookie.keys) {\n const hash = value ? this.options.cookie.keys.sign(value) : ''\n setCookie(res, `${name}:hash`, hash, cookieOptions)\n }\n }\n\n public getRequestMetadata(req: IncomingMessage) {\n return extractRequestMetadata(req, this.options)\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"device-manager.js","sourceRoot":"","sources":["../../src/device/device-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { z } from 'zod'\nimport { SESSION_FIXATION_MAX_AGE } from '../constants.js'\nimport { parseHttpCookies } from '../lib/http/index.js'\nimport {\n RequestMetadata,\n extractRequestMetadata,\n setCookie,\n} from '../lib/http/request.js'\nimport { DeviceData } from './device-data.js'\nimport { DeviceId, deviceIdSchema, generateDeviceId } from './device-id.js'\nimport { DeviceStore } from './device-store.js'\nimport { generateSessionId, sessionIdSchema } from './session-id.js'\n\n/**\n * @see {@link https://www.npmjs.com/package/keygrip | Keygrip}\n */\nexport const keygripSchema = z.object({\n sign: z.function().args(z.any()).returns(z.string()),\n verify: z.function().args(z.any(), z.string()).returns(z.boolean()),\n index: z.function().args(z.any(), z.string()).returns(z.number()),\n})\n\nexport const deviceManagerOptionsSchema = z.object({\n /**\n * Controls whether the IP address is read from the `X-Forwarded-For` header\n * (if `true`), or from the `req.socket.remoteAddress` property (if `false`).\n */\n trustProxy: z\n .function()\n .args<[addr: z.ZodString, i: z.ZodNumber]>(z.string(), z.number())\n .returns(z.boolean())\n .optional(),\n\n /**\n * Amount of time (in ms) after which session IDs will be rotated\n *\n * @default 300e3 // (5 minutes)\n */\n rotationRate: z.number().default(300e3),\n /**\n * Cookie options\n */\n cookie: z\n .object({\n keys: keygripSchema.optional(),\n /**\n * Amount of time (in ms) after which the session cookie will expire.\n * If set to `null`, the cookie will be a session cookie (deleted when the\n * browser is closed).\n *\n * @default 10 years\n */\n age: z\n .number()\n .nullable()\n .default(10 * 365.2 * 24 * 60 * 60e3),\n /**\n * Controls whether the cookie is only sent over HTTPS (if `true`), or also\n * over HTTP (if `false`). This should **NOT** be set to `false` in\n * production.\n */\n secure: z.boolean().default(true),\n /**\n * Controls whether the cookie is sent along with cross-site requests.\n *\n * @default 'lax'\n */\n sameSite: z.enum(['lax', 'strict']).default('lax'),\n })\n .default({}),\n})\n\nexport type DeviceManagerOptions = z.input<typeof deviceManagerOptionsSchema>\n\ntype CookieValue = {\n deviceId: DeviceId\n sessionId: string\n}\n\nexport type DeviceInfo = {\n deviceId: DeviceId\n deviceMetadata: RequestMetadata\n}\n\n/**\n * This class provides an abstraction for keeping track of DEVICE sessions. It\n * relies on a {@link DeviceStore} to persist session data and a cookie to\n * identify the session.\n */\nexport class DeviceManager {\n private readonly options: z.output<typeof deviceManagerOptionsSchema>\n\n constructor(\n private readonly store: DeviceStore,\n options: DeviceManagerOptions = {},\n ) {\n this.options = deviceManagerOptionsSchema.parse(options)\n }\n\n public async hasSession(req: IncomingMessage): Promise<boolean> {\n const cookies = await this.getCookies(req)\n return cookies !== null\n }\n\n public async load(\n req: IncomingMessage,\n res: ServerResponse,\n forceRotate = false,\n ): Promise<DeviceInfo> {\n const cookie = await this.getCookies(req)\n if (cookie) {\n return this.refresh(\n req,\n res,\n cookie.value,\n forceRotate || cookie.mustRotate,\n )\n } else {\n return this.create(req, res)\n }\n }\n\n private async create(\n req: IncomingMessage,\n res: ServerResponse,\n ): Promise<DeviceInfo> {\n const deviceMetadata = this.getRequestMetadata(req)\n\n const [deviceId, sessionId] = await Promise.all([\n generateDeviceId(),\n generateSessionId(),\n ] as const)\n\n await this.store.createDevice(deviceId, {\n sessionId,\n lastSeenAt: new Date(),\n userAgent: deviceMetadata.userAgent ?? null,\n ipAddress: deviceMetadata.ipAddress,\n })\n\n await this.setCookies(req, res, { deviceId, sessionId })\n\n return { deviceId, deviceMetadata }\n }\n\n private async refresh(\n req: IncomingMessage,\n res: ServerResponse,\n { deviceId, sessionId }: CookieValue,\n forceRotate = false,\n ): Promise<DeviceInfo> {\n const data = await this.store.readDevice(deviceId)\n if (!data) return this.create(req, res)\n\n const lastSeenAt = new Date(data.lastSeenAt)\n const age = Date.now() - lastSeenAt.getTime()\n\n if (sessionId !== data.sessionId) {\n if (age <= SESSION_FIXATION_MAX_AGE) {\n // The cookie was probably rotated by a concurrent request. Let's\n // update the cookie with the new sessionId.\n forceRotate = true\n } else {\n // Something's wrong. Let's create a new session.\n await this.store.deleteDevice(deviceId)\n return this.create(req, res)\n }\n }\n\n const deviceMetadata = this.getRequestMetadata(req)\n\n const shouldRotate =\n forceRotate ||\n deviceMetadata.ipAddress !== data.ipAddress ||\n deviceMetadata.userAgent !== data.userAgent ||\n age > this.options.rotationRate\n\n if (shouldRotate) {\n await this.rotate(req, res, deviceId, {\n ipAddress: deviceMetadata.ipAddress,\n userAgent: deviceMetadata.userAgent || data.userAgent,\n })\n }\n\n return { deviceId, deviceMetadata }\n }\n\n private async rotate(\n req: IncomingMessage,\n res: ServerResponse,\n deviceId: DeviceId,\n data?: Partial<Omit<DeviceData, 'sessionId' | 'lastSeenAt'>>,\n ): Promise<void> {\n const sessionId = await generateSessionId()\n\n await this.store.updateDevice(deviceId, {\n ...data,\n sessionId,\n lastSeenAt: new Date(),\n })\n\n await this.setCookies(req, res, { deviceId, sessionId })\n }\n\n private async getCookies(\n req: IncomingMessage,\n ): Promise<{ value: CookieValue; mustRotate: boolean } | null> {\n const cookies = parseHttpCookies(req)\n\n const device = this.parseCookie(cookies, `dev-id`, deviceIdSchema)\n const session = this.parseCookie(cookies, `ses-id`, sessionIdSchema)\n\n const deviceId = device?.value\n const sessionId = session?.value\n\n // Silently ignore invalid cookies\n if (!deviceId || !sessionId) {\n // If the device cookie is still present, let's cleanup the DB\n if (deviceId) await this.store.deleteDevice(deviceId)\n\n return null\n }\n\n return {\n value: { deviceId, sessionId },\n mustRotate: device.mustRotate || session.mustRotate,\n }\n }\n\n private parseCookie<T>(\n cookies: Record<string, string | undefined>,\n name: string,\n schema: z.ZodType<T> | z.ZodEffects<z.ZodTypeAny, T, string>,\n ): null | { value: T; mustRotate: boolean } {\n const rawValue = Object.hasOwn(cookies, name) ? cookies[name] : null\n if (!rawValue) return null\n\n const result = schema.safeParse(rawValue)\n if (!result.success) return null\n\n const value = result.data\n\n if (this.options.cookie.keys) {\n const hashName = `${name}:hash`\n\n const hash = Object.hasOwn(cookies, hashName) ? cookies[hashName] : null\n if (!hash) return null\n\n const idx = this.options.cookie.keys.index(rawValue, hash)\n if (idx < 0) return null\n\n return { value, mustRotate: idx !== 0 }\n }\n\n return { value, mustRotate: false }\n }\n\n private async setCookies(\n req: IncomingMessage,\n res: ServerResponse,\n { deviceId, sessionId }: CookieValue,\n ) {\n this.writeCookie(res, `dev-id`, deviceId)\n this.writeCookie(res, `ses-id`, sessionId)\n }\n\n private writeCookie(res: ServerResponse, name: string, value?: string) {\n const cookieOptions = {\n maxAge: value\n ? this.options.cookie.age == null\n ? undefined\n : this.options.cookie.age / 1000\n : 0,\n httpOnly: true,\n path: '/',\n secure: this.options.cookie.secure !== false,\n sameSite: this.options.cookie.sameSite,\n } as const\n\n setCookie(res, name, value || '', cookieOptions)\n\n if (this.options.cookie.keys) {\n const hash = value ? this.options.cookie.keys.sign(value) : ''\n setCookie(res, `${name}:hash`, hash, cookieOptions)\n }\n }\n\n public getRequestMetadata(req: IncomingMessage) {\n return extractRequestMetadata(req, this.options)\n }\n}\n"]}
|
|
@@ -1,34 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
-
};
|
|
16
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.isDeviceStore = void 0;
|
|
18
|
-
exports.asDeviceStore = asDeviceStore;
|
|
19
|
-
const type_js_1 = require("../lib/util/type.js");
|
|
1
|
+
import { buildInterfaceChecker } from '../lib/util/type.js';
|
|
20
2
|
// Export all types needed to implement the DeviceStore interface
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
3
|
+
export * from './device-data.js';
|
|
4
|
+
export * from './device-id.js';
|
|
5
|
+
export * from './session-id.js';
|
|
6
|
+
export const isDeviceStore = buildInterfaceChecker([
|
|
25
7
|
'createDevice',
|
|
26
8
|
'readDevice',
|
|
27
9
|
'updateDevice',
|
|
28
10
|
'deleteDevice',
|
|
29
11
|
]);
|
|
30
|
-
function asDeviceStore(implementation) {
|
|
31
|
-
if (!implementation || !
|
|
12
|
+
export function asDeviceStore(implementation) {
|
|
13
|
+
if (!implementation || !isDeviceStore(implementation)) {
|
|
32
14
|
throw new Error('Invalid DeviceStore implementation');
|
|
33
15
|
}
|
|
34
16
|
return implementation;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device-store.js","sourceRoot":"","sources":["../../src/device/device-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"device-store.js","sourceRoot":"","sources":["../../src/device/device-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAItE,iEAAiE;AACjE,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAW/B,MAAM,CAAC,MAAM,aAAa,GAAG,qBAAqB,CAAc;IAC9D,cAAc;IACd,YAAY;IACZ,cAAc;IACd,cAAc;CACf,CAAC,CAAA;AAEF,MAAM,UAAU,aAAa,CAAI,cAAiB;IAChD,IAAI,CAAC,cAAc,IAAI,CAAC,aAAa,CAAC,cAAc,CAAC,EAAE,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAA;IACvD,CAAC;IACD,OAAO,cAAc,CAAA;AACvB,CAAC","sourcesContent":["import { Awaitable, buildInterfaceChecker } from '../lib/util/type.js'\nimport { DeviceData } from './device-data.js'\nimport { DeviceId } from './device-id.js'\n\n// Export all types needed to implement the DeviceStore interface\nexport * from './device-data.js'\nexport * from './device-id.js'\nexport * from './session-id.js'\n\nexport type { Awaitable }\n\nexport interface DeviceStore {\n createDevice(deviceId: DeviceId, data: DeviceData): Awaitable<void>\n readDevice(deviceId: DeviceId): Awaitable<DeviceData | null>\n updateDevice(deviceId: DeviceId, data: Partial<DeviceData>): Awaitable<void>\n deleteDevice(deviceId: DeviceId): Awaitable<void>\n}\n\nexport const isDeviceStore = buildInterfaceChecker<DeviceStore>([\n 'createDevice',\n 'readDevice',\n 'updateDevice',\n 'deleteDevice',\n])\n\nexport function asDeviceStore<V>(implementation: V): V & DeviceStore {\n if (!implementation || !isDeviceStore(implementation)) {\n throw new Error('Invalid DeviceStore implementation')\n }\n return implementation\n}\n"]}
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const crypto_js_1 = require("../lib/util/crypto.js");
|
|
7
|
-
exports.SESSION_ID_LENGTH = constants_js_1.SESSION_ID_PREFIX.length + constants_js_1.SESSION_ID_BYTES_LENGTH * 2; // hex encoding
|
|
8
|
-
exports.sessionIdSchema = zod_1.z
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { SESSION_ID_BYTES_LENGTH, SESSION_ID_PREFIX } from '../constants.js';
|
|
3
|
+
import { randomHexId } from '../lib/util/crypto.js';
|
|
4
|
+
export const SESSION_ID_LENGTH = SESSION_ID_PREFIX.length + SESSION_ID_BYTES_LENGTH * 2; // hex encoding
|
|
5
|
+
export const sessionIdSchema = z
|
|
9
6
|
.string()
|
|
10
|
-
.length(
|
|
11
|
-
.refine((v) => v.startsWith(
|
|
7
|
+
.length(SESSION_ID_LENGTH)
|
|
8
|
+
.refine((v) => v.startsWith(SESSION_ID_PREFIX), {
|
|
12
9
|
message: `Invalid session ID format`,
|
|
13
10
|
});
|
|
14
|
-
const generateSessionId = async () => {
|
|
15
|
-
return `${
|
|
11
|
+
export const generateSessionId = async () => {
|
|
12
|
+
return `${SESSION_ID_PREFIX}${await randomHexId(SESSION_ID_BYTES_LENGTH)}`;
|
|
16
13
|
};
|
|
17
|
-
exports.generateSessionId = generateSessionId;
|
|
18
14
|
//# sourceMappingURL=session-id.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-id.js","sourceRoot":"","sources":["../../src/device/session-id.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"session-id.js","sourceRoot":"","sources":["../../src/device/session-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAEnD,MAAM,CAAC,MAAM,iBAAiB,GAC5B,iBAAiB,CAAC,MAAM,GAAG,uBAAuB,GAAG,CAAC,CAAA,CAAC,eAAe;AAExE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC;KAC7B,MAAM,EAAE;KACR,MAAM,CAAC,iBAAiB,CAAC;KACzB,MAAM,CACL,CAAC,CAAC,EAA+C,EAAE,CACjD,CAAC,CAAC,UAAU,CAAC,iBAAiB,CAAC,EACjC;IACE,OAAO,EAAE,2BAA2B;CACrC,CACF,CAAA;AAEH,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,IAAwB,EAAE;IAC9D,OAAO,GAAG,iBAAiB,GAAG,MAAM,WAAW,CAAC,uBAAuB,CAAC,EAAE,CAAA;AAC5E,CAAC,CAAA","sourcesContent":["import { z } from 'zod'\nimport { SESSION_ID_BYTES_LENGTH, SESSION_ID_PREFIX } from '../constants.js'\nimport { randomHexId } from '../lib/util/crypto.js'\n\nexport const SESSION_ID_LENGTH =\n SESSION_ID_PREFIX.length + SESSION_ID_BYTES_LENGTH * 2 // hex encoding\n\nexport const sessionIdSchema = z\n .string()\n .length(SESSION_ID_LENGTH)\n .refine(\n (v): v is `${typeof SESSION_ID_PREFIX}${string}` =>\n v.startsWith(SESSION_ID_PREFIX),\n {\n message: `Invalid session ID format`,\n },\n )\nexport type SessionId = z.infer<typeof sessionIdSchema>\nexport const generateSessionId = async (): Promise<SessionId> => {\n return `${SESSION_ID_PREFIX}${await randomHexId(SESSION_ID_BYTES_LENGTH)}`\n}\n"]}
|
|
@@ -9,13 +9,13 @@ export declare const dpopManagerOptionsSchema: z.ZodObject<{
|
|
|
9
9
|
* all nonces (typically useful when multiple instances are running). Leave
|
|
10
10
|
* undefined to generate a random seed at startup.
|
|
11
11
|
*/
|
|
12
|
-
dpopSecret: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<false>, z.ZodUnion<[z.ZodEffects<z.ZodType<Uint8Array<
|
|
12
|
+
dpopSecret: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<false>, z.ZodUnion<[z.ZodEffects<z.ZodType<Uint8Array<ArrayBufferLike>, z.ZodTypeDef, Uint8Array<ArrayBufferLike>>, Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>>, z.ZodEffects<z.ZodString, Uint8Array<ArrayBufferLike>, string>]>]>>;
|
|
13
13
|
dpopRotationInterval: z.ZodOptional<z.ZodNumber>;
|
|
14
14
|
}, "strip", z.ZodTypeAny, {
|
|
15
|
-
dpopSecret?: false | Uint8Array<ArrayBufferLike> |
|
|
15
|
+
dpopSecret?: false | Uint8Array<ArrayBufferLike> | undefined;
|
|
16
16
|
dpopRotationInterval?: number | undefined;
|
|
17
17
|
}, {
|
|
18
|
-
dpopSecret?: string | false | Uint8Array<
|
|
18
|
+
dpopSecret?: string | false | Uint8Array<ArrayBufferLike> | undefined;
|
|
19
19
|
dpopRotationInterval?: number | undefined;
|
|
20
20
|
}>;
|
|
21
21
|
export type DpopManagerOptions = z.input<typeof dpopManagerOptionsSchema>;
|
|
@@ -1,35 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
Object.defineProperty(exports, "DpopNonce", { enumerable: true, get: function () { return dpop_nonce_js_1.DpopNonce; } });
|
|
14
|
-
const { JOSEError } = jose_1.errors;
|
|
15
|
-
exports.dpopManagerOptionsSchema = zod_1.z.object({
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { EmbeddedJWK, calculateJwkThumbprint, errors, jwtVerify } from 'jose';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { ValidationError } from '@atproto/jwk';
|
|
5
|
+
import { DPOP_NONCE_MAX_AGE } from '../constants.js';
|
|
6
|
+
import { InvalidDpopProofError } from '../errors/invalid-dpop-proof-error.js';
|
|
7
|
+
import { UseDpopNonceError } from '../errors/use-dpop-nonce-error.js';
|
|
8
|
+
import { ifURL } from '../lib/util/cast.js';
|
|
9
|
+
import { DpopNonce, dpopSecretSchema, rotationIntervalSchema, } from './dpop-nonce.js';
|
|
10
|
+
const { JOSEError } = errors;
|
|
11
|
+
export { DpopNonce };
|
|
12
|
+
export const dpopManagerOptionsSchema = z.object({
|
|
16
13
|
/**
|
|
17
14
|
* Set this to `false` to disable the use of nonces in DPoP proofs. Set this
|
|
18
15
|
* to a secret Uint8Array or hex encoded string to use a predictable seed for
|
|
19
16
|
* all nonces (typically useful when multiple instances are running). Leave
|
|
20
17
|
* undefined to generate a random seed at startup.
|
|
21
18
|
*/
|
|
22
|
-
dpopSecret:
|
|
23
|
-
dpopRotationInterval:
|
|
19
|
+
dpopSecret: z.union([z.literal(false), dpopSecretSchema]).optional(),
|
|
20
|
+
dpopRotationInterval: rotationIntervalSchema.optional(),
|
|
24
21
|
});
|
|
25
|
-
class DpopManager {
|
|
26
|
-
dpopNonce;
|
|
22
|
+
export class DpopManager {
|
|
27
23
|
constructor(options = {}) {
|
|
28
|
-
const { dpopSecret, dpopRotationInterval } =
|
|
24
|
+
const { dpopSecret, dpopRotationInterval } = dpopManagerOptionsSchema.parse(options);
|
|
29
25
|
this.dpopNonce =
|
|
30
26
|
dpopSecret === false
|
|
31
27
|
? undefined
|
|
32
|
-
: new
|
|
28
|
+
: new DpopNonce(dpopSecret, dpopRotationInterval);
|
|
33
29
|
}
|
|
34
30
|
nextNonce() {
|
|
35
31
|
return this.dpopNonce?.next();
|
|
@@ -45,10 +41,10 @@ class DpopManager {
|
|
|
45
41
|
const proof = extractProof(httpHeaders);
|
|
46
42
|
if (!proof)
|
|
47
43
|
return null;
|
|
48
|
-
const { protectedHeader, payload } = await
|
|
44
|
+
const { protectedHeader, payload } = await jwtVerify(proof, EmbeddedJWK, {
|
|
49
45
|
typ: 'dpop+jwt',
|
|
50
46
|
maxTokenAge: 10, // Will ensure presence & validity of "iat" claim
|
|
51
|
-
clockTolerance:
|
|
47
|
+
clockTolerance: DPOP_NONCE_MAX_AGE / 1e3,
|
|
52
48
|
}).catch((err) => {
|
|
53
49
|
throw wrapInvalidDpopProofError(err, 'Failed to verify DPoP proof');
|
|
54
50
|
});
|
|
@@ -64,17 +60,17 @@ class DpopManager {
|
|
|
64
60
|
// we decide to drop legacy support.
|
|
65
61
|
const { ath, htm, htu, jti, nonce } = payload;
|
|
66
62
|
if (nonce !== undefined && typeof nonce !== 'string') {
|
|
67
|
-
throw new
|
|
63
|
+
throw new InvalidDpopProofError('Invalid DPoP "nonce" type');
|
|
68
64
|
}
|
|
69
65
|
if (!jti || typeof jti !== 'string') {
|
|
70
|
-
throw new
|
|
66
|
+
throw new InvalidDpopProofError('DPoP "jti" missing');
|
|
71
67
|
}
|
|
72
68
|
// Note rfc9110#section-9.1 states that the method name is case-sensitive
|
|
73
69
|
if (!htm || htm !== httpMethod) {
|
|
74
|
-
throw new
|
|
70
|
+
throw new InvalidDpopProofError('DPoP "htm" mismatch');
|
|
75
71
|
}
|
|
76
72
|
if (!htu || typeof htu !== 'string') {
|
|
77
|
-
throw new
|
|
73
|
+
throw new InvalidDpopProofError('Invalid DPoP "htu" type');
|
|
78
74
|
}
|
|
79
75
|
// > To reduce the likelihood of false negatives, servers SHOULD employ
|
|
80
76
|
// > syntax-based normalization (Section 6.2.2 of [RFC3986]) and
|
|
@@ -83,27 +79,27 @@ class DpopManager {
|
|
|
83
79
|
//
|
|
84
80
|
// RFC9449 section 4.3. Checking DPoP Proofs - https://datatracker.ietf.org/doc/html/rfc9449#section-4.3
|
|
85
81
|
if (!htu || parseHtu(htu) !== normalizeHtuUrl(httpUrl)) {
|
|
86
|
-
throw new
|
|
82
|
+
throw new InvalidDpopProofError('DPoP "htu" mismatch');
|
|
87
83
|
}
|
|
88
84
|
if (!nonce && this.dpopNonce) {
|
|
89
|
-
throw new
|
|
85
|
+
throw new UseDpopNonceError();
|
|
90
86
|
}
|
|
91
87
|
if (nonce && !this.dpopNonce?.check(nonce)) {
|
|
92
|
-
throw new
|
|
88
|
+
throw new UseDpopNonceError('DPoP "nonce" mismatch');
|
|
93
89
|
}
|
|
94
90
|
if (accessToken) {
|
|
95
|
-
const accessTokenHash =
|
|
91
|
+
const accessTokenHash = createHash('sha256').update(accessToken).digest();
|
|
96
92
|
if (ath !== accessTokenHash.toString('base64url')) {
|
|
97
|
-
throw new
|
|
93
|
+
throw new InvalidDpopProofError('DPoP "ath" mismatch');
|
|
98
94
|
}
|
|
99
95
|
}
|
|
100
96
|
else if (ath !== undefined) {
|
|
101
|
-
throw new
|
|
97
|
+
throw new InvalidDpopProofError('DPoP "ath" claim not allowed');
|
|
102
98
|
}
|
|
103
99
|
// @NOTE we can assert there is a jwk because the jwtVerify used the
|
|
104
100
|
// EmbeddedJWK key getter mechanism.
|
|
105
101
|
const jwk = protectedHeader.jwk;
|
|
106
|
-
const jkt = await
|
|
102
|
+
const jkt = await calculateJwkThumbprint(jwk, 'sha256').catch((err) => {
|
|
107
103
|
throw wrapInvalidDpopProofError(err, 'Failed to calculate jkt');
|
|
108
104
|
});
|
|
109
105
|
// @NOTE We freeze the proof to prevent accidental modification (esp. from
|
|
@@ -111,20 +107,19 @@ class DpopManager {
|
|
|
111
107
|
return Object.freeze({ jti, jkt, htm, htu });
|
|
112
108
|
}
|
|
113
109
|
}
|
|
114
|
-
exports.DpopManager = DpopManager;
|
|
115
110
|
function extractProof(httpHeaders) {
|
|
116
111
|
const dpopHeader = httpHeaders['dpop'];
|
|
117
112
|
switch (typeof dpopHeader) {
|
|
118
113
|
case 'string':
|
|
119
114
|
if (dpopHeader)
|
|
120
115
|
return dpopHeader;
|
|
121
|
-
throw new
|
|
116
|
+
throw new InvalidDpopProofError('DPoP header cannot be empty');
|
|
122
117
|
case 'object':
|
|
123
118
|
// @NOTE the "0" case should never happen a node.js HTTP server will only
|
|
124
119
|
// return an array if the header is set multiple times.
|
|
125
120
|
if (dpopHeader.length === 1 && dpopHeader[0])
|
|
126
121
|
return dpopHeader[0];
|
|
127
|
-
throw new
|
|
122
|
+
throw new InvalidDpopProofError('DPoP header must contain a single proof');
|
|
128
123
|
default:
|
|
129
124
|
return null;
|
|
130
125
|
}
|
|
@@ -145,18 +140,18 @@ function normalizeHtuUrl(url) {
|
|
|
145
140
|
return url.origin + url.pathname;
|
|
146
141
|
}
|
|
147
142
|
function parseHtu(htu) {
|
|
148
|
-
const url =
|
|
143
|
+
const url = ifURL(htu);
|
|
149
144
|
if (!url) {
|
|
150
|
-
throw new
|
|
145
|
+
throw new InvalidDpopProofError('DPoP "htu" is not a valid URL');
|
|
151
146
|
}
|
|
152
147
|
// @NOTE the checks bellow can be removed once once jwtPayloadSchema is used
|
|
153
148
|
// to validate the DPoP proof payload as it already performs these checks
|
|
154
149
|
// (though the htuSchema).
|
|
155
150
|
if (url.password || url.username) {
|
|
156
|
-
throw new
|
|
151
|
+
throw new InvalidDpopProofError('DPoP "htu" must not contain credentials');
|
|
157
152
|
}
|
|
158
153
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
159
|
-
throw new
|
|
154
|
+
throw new InvalidDpopProofError('DPoP "htu" must be http or https');
|
|
160
155
|
}
|
|
161
156
|
// @NOTE For legacy & backwards compatibility reason, we allow a query and
|
|
162
157
|
// fragment in the DPoP proof's htu. This is not a standard behavior as the
|
|
@@ -165,9 +160,9 @@ function parseHtu(htu) {
|
|
|
165
160
|
return normalizeHtuUrl(url);
|
|
166
161
|
}
|
|
167
162
|
function wrapInvalidDpopProofError(err, title) {
|
|
168
|
-
const msg = err instanceof JOSEError || err instanceof
|
|
163
|
+
const msg = err instanceof JOSEError || err instanceof ValidationError
|
|
169
164
|
? `${title}: ${err.message}`
|
|
170
165
|
: title;
|
|
171
|
-
return new
|
|
166
|
+
return new InvalidDpopProofError(msg, err);
|
|
172
167
|
}
|
|
173
168
|
//# sourceMappingURL=dpop-manager.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dpop-manager.js","sourceRoot":"","sources":["../../src/dpop/dpop-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto'\nimport { EmbeddedJWK, calculateJwkThumbprint, errors, jwtVerify } from 'jose'\nimport { z } from 'zod'\nimport { ValidationError } from '@atproto/jwk'\nimport { DPOP_NONCE_MAX_AGE } from '../constants.js'\nimport { InvalidDpopProofError } from '../errors/invalid-dpop-proof-error.js'\nimport { UseDpopNonceError } from '../errors/use-dpop-nonce-error.js'\nimport { ifURL } from '../lib/util/cast.js'\nimport {\n DpopNonce,\n DpopSecret,\n dpopSecretSchema,\n rotationIntervalSchema,\n} from './dpop-nonce.js'\nimport { DpopProof } from './dpop-proof.js'\n\nconst { JOSEError } = errors\n\nexport { DpopNonce, type DpopSecret }\n\nexport const dpopManagerOptionsSchema = z.object({\n /**\n * Set this to `false` to disable the use of nonces in DPoP proofs. Set this\n * to a secret Uint8Array or hex encoded string to use a predictable seed for\n * all nonces (typically useful when multiple instances are running). Leave\n * undefined to generate a random seed at startup.\n */\n dpopSecret: z.union([z.literal(false), dpopSecretSchema]).optional(),\n dpopRotationInterval: rotationIntervalSchema.optional(),\n})\nexport type DpopManagerOptions = z.input<typeof dpopManagerOptionsSchema>\n\nexport class DpopManager {\n protected readonly dpopNonce?: DpopNonce\n\n constructor(options: DpopManagerOptions = {}) {\n const { dpopSecret, dpopRotationInterval } =\n dpopManagerOptionsSchema.parse(options)\n this.dpopNonce =\n dpopSecret === false\n ? undefined\n : new DpopNonce(dpopSecret, dpopRotationInterval)\n }\n\n nextNonce(): string | undefined {\n return this.dpopNonce?.next()\n }\n\n /**\n * @see {@link https://datatracker.ietf.org/doc/html/rfc9449#section-4.3}\n */\n async checkProof(\n httpMethod: string,\n httpUrl: Readonly<URL>,\n httpHeaders: Record<string, undefined | string | string[]>,\n accessToken?: string,\n ): Promise<null | DpopProof> {\n // Fool proofing against use of empty string\n if (!httpMethod) {\n throw new TypeError('HTTP method is required')\n }\n\n const proof = extractProof(httpHeaders)\n if (!proof) return null\n\n const { protectedHeader, payload } = await jwtVerify(proof, EmbeddedJWK, {\n typ: 'dpop+jwt',\n maxTokenAge: 10, // Will ensure presence & validity of \"iat\" claim\n clockTolerance: DPOP_NONCE_MAX_AGE / 1e3,\n }).catch((err) => {\n throw wrapInvalidDpopProofError(err, 'Failed to verify DPoP proof')\n })\n\n // @NOTE For legacy & backwards compatibility reason, we cannot use\n // `jwtPayloadSchema` here as it will reject DPoP proofs containing a query\n // or fragment component in the \"htu\" claim.\n\n // const { ath, htm, htu, jti, nonce } = await jwtPayloadSchema\n // .parseAsync(payload)\n // .catch((err) => {\n // throw buildInvalidDpopProofError('Invalid DPoP proof', err)\n // })\n\n // @TODO Uncomment previous lines (and remove redundant checks bellow) once\n // we decide to drop legacy support.\n const { ath, htm, htu, jti, nonce } = payload\n\n if (nonce !== undefined && typeof nonce !== 'string') {\n throw new InvalidDpopProofError('Invalid DPoP \"nonce\" type')\n }\n\n if (!jti || typeof jti !== 'string') {\n throw new InvalidDpopProofError('DPoP \"jti\" missing')\n }\n\n // Note rfc9110#section-9.1 states that the method name is case-sensitive\n if (!htm || htm !== httpMethod) {\n throw new InvalidDpopProofError('DPoP \"htm\" mismatch')\n }\n\n if (!htu || typeof htu !== 'string') {\n throw new InvalidDpopProofError('Invalid DPoP \"htu\" type')\n }\n\n // > To reduce the likelihood of false negatives, servers SHOULD employ\n // > syntax-based normalization (Section 6.2.2 of [RFC3986]) and\n // > scheme-based normalization (Section 6.2.3 of [RFC3986]) before\n // > comparing the htu claim.\n //\n // RFC9449 section 4.3. Checking DPoP Proofs - https://datatracker.ietf.org/doc/html/rfc9449#section-4.3\n if (!htu || parseHtu(htu) !== normalizeHtuUrl(httpUrl)) {\n throw new InvalidDpopProofError('DPoP \"htu\" mismatch')\n }\n\n if (!nonce && this.dpopNonce) {\n throw new UseDpopNonceError()\n }\n\n if (nonce && !this.dpopNonce?.check(nonce)) {\n throw new UseDpopNonceError('DPoP \"nonce\" mismatch')\n }\n\n if (accessToken) {\n const accessTokenHash = createHash('sha256').update(accessToken).digest()\n if (ath !== accessTokenHash.toString('base64url')) {\n throw new InvalidDpopProofError('DPoP \"ath\" mismatch')\n }\n } else if (ath !== undefined) {\n throw new InvalidDpopProofError('DPoP \"ath\" claim not allowed')\n }\n\n // @NOTE we can assert there is a jwk because the jwtVerify used the\n // EmbeddedJWK key getter mechanism.\n const jwk = protectedHeader.jwk!\n const jkt = await calculateJwkThumbprint(jwk, 'sha256').catch((err) => {\n throw wrapInvalidDpopProofError(err, 'Failed to calculate jkt')\n })\n\n // @NOTE We freeze the proof to prevent accidental modification (esp. from\n // hooks).\n return Object.freeze({ jti, jkt, htm, htu })\n }\n}\n\nfunction extractProof(\n httpHeaders: Record<string, undefined | string | string[]>,\n): string | null {\n const dpopHeader = httpHeaders['dpop']\n switch (typeof dpopHeader) {\n case 'string':\n if (dpopHeader) return dpopHeader\n throw new InvalidDpopProofError('DPoP header cannot be empty')\n case 'object':\n // @NOTE the \"0\" case should never happen a node.js HTTP server will only\n // return an array if the header is set multiple times.\n if (dpopHeader.length === 1 && dpopHeader[0]) return dpopHeader[0]!\n throw new InvalidDpopProofError('DPoP header must contain a single proof')\n default:\n return null\n }\n}\n\n/**\n * Constructs the HTTP URI (htu) claim as defined in RFC9449.\n *\n * The htu claim is the normalized URL of the HTTP request, excluding the query\n * string and fragment. This function ensures that the URL is normalized by\n * removing the search and hash components, as well as by using an URL object to\n * simplify the pathname (e.g. removing dot segments).\n *\n * @returns The normalized URL as a string.\n * @see {@link https://datatracker.ietf.org/doc/html/rfc9449#section-4.3}\n */\nfunction normalizeHtuUrl(url: Readonly<URL>): string {\n // NodeJS's `URL` normalizes the pathname, so we can just use that.\n return url.origin + url.pathname\n}\n\nfunction parseHtu(htu: string): string {\n const url = ifURL(htu)\n if (!url) {\n throw new InvalidDpopProofError('DPoP \"htu\" is not a valid URL')\n }\n\n // @NOTE the checks bellow can be removed once once jwtPayloadSchema is used\n // to validate the DPoP proof payload as it already performs these checks\n // (though the htuSchema).\n\n if (url.password || url.username) {\n throw new InvalidDpopProofError('DPoP \"htu\" must not contain credentials')\n }\n\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n throw new InvalidDpopProofError('DPoP \"htu\" must be http or https')\n }\n\n // @NOTE For legacy & backwards compatibility reason, we allow a query and\n // fragment in the DPoP proof's htu. This is not a standard behavior as the\n // htu is not supposed to contain query or fragment.\n\n // NodeJS's `URL` normalizes the pathname.\n return normalizeHtuUrl(url)\n}\n\nfunction wrapInvalidDpopProofError(\n err: unknown,\n title: string,\n): InvalidDpopProofError {\n const msg =\n err instanceof JOSEError || err instanceof ValidationError\n ? `${title}: ${err.message}`\n : title\n return new InvalidDpopProofError(msg, err)\n}\n"]}
|
|
1
|
+
{"version":3,"file":"dpop-manager.js","sourceRoot":"","sources":["../../src/dpop/dpop-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto'\nimport { EmbeddedJWK, calculateJwkThumbprint, errors, jwtVerify } from 'jose'\nimport { z } from 'zod'\nimport { ValidationError } from '@atproto/jwk'\nimport { DPOP_NONCE_MAX_AGE } from '../constants.js'\nimport { InvalidDpopProofError } from '../errors/invalid-dpop-proof-error.js'\nimport { UseDpopNonceError } from '../errors/use-dpop-nonce-error.js'\nimport { ifURL } from '../lib/util/cast.js'\nimport {\n DpopNonce,\n DpopSecret,\n dpopSecretSchema,\n rotationIntervalSchema,\n} from './dpop-nonce.js'\nimport { DpopProof } from './dpop-proof.js'\n\nconst { JOSEError } = errors\n\nexport { DpopNonce, type DpopSecret }\n\nexport const dpopManagerOptionsSchema = z.object({\n /**\n * Set this to `false` to disable the use of nonces in DPoP proofs. Set this\n * to a secret Uint8Array or hex encoded string to use a predictable seed for\n * all nonces (typically useful when multiple instances are running). Leave\n * undefined to generate a random seed at startup.\n */\n dpopSecret: z.union([z.literal(false), dpopSecretSchema]).optional(),\n dpopRotationInterval: rotationIntervalSchema.optional(),\n})\nexport type DpopManagerOptions = z.input<typeof dpopManagerOptionsSchema>\n\nexport class DpopManager {\n protected readonly dpopNonce?: DpopNonce\n\n constructor(options: DpopManagerOptions = {}) {\n const { dpopSecret, dpopRotationInterval } =\n dpopManagerOptionsSchema.parse(options)\n this.dpopNonce =\n dpopSecret === false\n ? undefined\n : new DpopNonce(dpopSecret, dpopRotationInterval)\n }\n\n nextNonce(): string | undefined {\n return this.dpopNonce?.next()\n }\n\n /**\n * @see {@link https://datatracker.ietf.org/doc/html/rfc9449#section-4.3}\n */\n async checkProof(\n httpMethod: string,\n httpUrl: Readonly<URL>,\n httpHeaders: Record<string, undefined | string | string[]>,\n accessToken?: string,\n ): Promise<null | DpopProof> {\n // Fool proofing against use of empty string\n if (!httpMethod) {\n throw new TypeError('HTTP method is required')\n }\n\n const proof = extractProof(httpHeaders)\n if (!proof) return null\n\n const { protectedHeader, payload } = await jwtVerify(proof, EmbeddedJWK, {\n typ: 'dpop+jwt',\n maxTokenAge: 10, // Will ensure presence & validity of \"iat\" claim\n clockTolerance: DPOP_NONCE_MAX_AGE / 1e3,\n }).catch((err) => {\n throw wrapInvalidDpopProofError(err, 'Failed to verify DPoP proof')\n })\n\n // @NOTE For legacy & backwards compatibility reason, we cannot use\n // `jwtPayloadSchema` here as it will reject DPoP proofs containing a query\n // or fragment component in the \"htu\" claim.\n\n // const { ath, htm, htu, jti, nonce } = await jwtPayloadSchema\n // .parseAsync(payload)\n // .catch((err) => {\n // throw buildInvalidDpopProofError('Invalid DPoP proof', err)\n // })\n\n // @TODO Uncomment previous lines (and remove redundant checks bellow) once\n // we decide to drop legacy support.\n const { ath, htm, htu, jti, nonce } = payload\n\n if (nonce !== undefined && typeof nonce !== 'string') {\n throw new InvalidDpopProofError('Invalid DPoP \"nonce\" type')\n }\n\n if (!jti || typeof jti !== 'string') {\n throw new InvalidDpopProofError('DPoP \"jti\" missing')\n }\n\n // Note rfc9110#section-9.1 states that the method name is case-sensitive\n if (!htm || htm !== httpMethod) {\n throw new InvalidDpopProofError('DPoP \"htm\" mismatch')\n }\n\n if (!htu || typeof htu !== 'string') {\n throw new InvalidDpopProofError('Invalid DPoP \"htu\" type')\n }\n\n // > To reduce the likelihood of false negatives, servers SHOULD employ\n // > syntax-based normalization (Section 6.2.2 of [RFC3986]) and\n // > scheme-based normalization (Section 6.2.3 of [RFC3986]) before\n // > comparing the htu claim.\n //\n // RFC9449 section 4.3. Checking DPoP Proofs - https://datatracker.ietf.org/doc/html/rfc9449#section-4.3\n if (!htu || parseHtu(htu) !== normalizeHtuUrl(httpUrl)) {\n throw new InvalidDpopProofError('DPoP \"htu\" mismatch')\n }\n\n if (!nonce && this.dpopNonce) {\n throw new UseDpopNonceError()\n }\n\n if (nonce && !this.dpopNonce?.check(nonce)) {\n throw new UseDpopNonceError('DPoP \"nonce\" mismatch')\n }\n\n if (accessToken) {\n const accessTokenHash = createHash('sha256').update(accessToken).digest()\n if (ath !== accessTokenHash.toString('base64url')) {\n throw new InvalidDpopProofError('DPoP \"ath\" mismatch')\n }\n } else if (ath !== undefined) {\n throw new InvalidDpopProofError('DPoP \"ath\" claim not allowed')\n }\n\n // @NOTE we can assert there is a jwk because the jwtVerify used the\n // EmbeddedJWK key getter mechanism.\n const jwk = protectedHeader.jwk!\n const jkt = await calculateJwkThumbprint(jwk, 'sha256').catch((err) => {\n throw wrapInvalidDpopProofError(err, 'Failed to calculate jkt')\n })\n\n // @NOTE We freeze the proof to prevent accidental modification (esp. from\n // hooks).\n return Object.freeze({ jti, jkt, htm, htu })\n }\n}\n\nfunction extractProof(\n httpHeaders: Record<string, undefined | string | string[]>,\n): string | null {\n const dpopHeader = httpHeaders['dpop']\n switch (typeof dpopHeader) {\n case 'string':\n if (dpopHeader) return dpopHeader\n throw new InvalidDpopProofError('DPoP header cannot be empty')\n case 'object':\n // @NOTE the \"0\" case should never happen a node.js HTTP server will only\n // return an array if the header is set multiple times.\n if (dpopHeader.length === 1 && dpopHeader[0]) return dpopHeader[0]!\n throw new InvalidDpopProofError('DPoP header must contain a single proof')\n default:\n return null\n }\n}\n\n/**\n * Constructs the HTTP URI (htu) claim as defined in RFC9449.\n *\n * The htu claim is the normalized URL of the HTTP request, excluding the query\n * string and fragment. This function ensures that the URL is normalized by\n * removing the search and hash components, as well as by using an URL object to\n * simplify the pathname (e.g. removing dot segments).\n *\n * @returns The normalized URL as a string.\n * @see {@link https://datatracker.ietf.org/doc/html/rfc9449#section-4.3}\n */\nfunction normalizeHtuUrl(url: Readonly<URL>): string {\n // NodeJS's `URL` normalizes the pathname, so we can just use that.\n return url.origin + url.pathname\n}\n\nfunction parseHtu(htu: string): string {\n const url = ifURL(htu)\n if (!url) {\n throw new InvalidDpopProofError('DPoP \"htu\" is not a valid URL')\n }\n\n // @NOTE the checks bellow can be removed once once jwtPayloadSchema is used\n // to validate the DPoP proof payload as it already performs these checks\n // (though the htuSchema).\n\n if (url.password || url.username) {\n throw new InvalidDpopProofError('DPoP \"htu\" must not contain credentials')\n }\n\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n throw new InvalidDpopProofError('DPoP \"htu\" must be http or https')\n }\n\n // @NOTE For legacy & backwards compatibility reason, we allow a query and\n // fragment in the DPoP proof's htu. This is not a standard behavior as the\n // htu is not supposed to contain query or fragment.\n\n // NodeJS's `URL` normalizes the pathname.\n return normalizeHtuUrl(url)\n}\n\nfunction wrapInvalidDpopProofError(\n err: unknown,\n title: string,\n): InvalidDpopProofError {\n const msg =\n err instanceof JOSEError || err instanceof ValidationError\n ? `${title}: ${err.message}`\n : title\n return new InvalidDpopProofError(msg, err)\n}\n"]}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export declare const rotationIntervalSchema: z.ZodNumber;
|
|
3
|
-
export declare const secretBytesSchema: z.ZodEffects<z.ZodType<Uint8Array<
|
|
3
|
+
export declare const secretBytesSchema: z.ZodEffects<z.ZodType<Uint8Array<ArrayBufferLike>, z.ZodTypeDef, Uint8Array<ArrayBufferLike>>, Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>>;
|
|
4
4
|
export declare const secretHexSchema: z.ZodEffects<z.ZodString, Uint8Array<ArrayBufferLike>, string>;
|
|
5
|
-
export declare const dpopSecretSchema: z.ZodUnion<[z.ZodEffects<z.ZodType<Uint8Array<
|
|
5
|
+
export declare const dpopSecretSchema: z.ZodUnion<[z.ZodEffects<z.ZodType<Uint8Array<ArrayBufferLike>, z.ZodTypeDef, Uint8Array<ArrayBufferLike>>, Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>>, z.ZodEffects<z.ZodString, Uint8Array<ArrayBufferLike>, string>]>;
|
|
6
6
|
export type DpopSecret = z.input<typeof dpopSecretSchema>;
|
|
7
7
|
export declare class DpopNonce {
|
|
8
8
|
#private;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dpop-nonce.d.ts","sourceRoot":"","sources":["../../src/dpop/dpop-nonce.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAMvB,eAAO,MAAM,sBAAsB,aAIN,CAAA;AAI7B,eAAO,MAAM,iBAAiB,
|
|
1
|
+
{"version":3,"file":"dpop-nonce.d.ts","sourceRoot":"","sources":["../../src/dpop/dpop-nonce.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAMvB,eAAO,MAAM,sBAAsB,aAIN,CAAA;AAI7B,eAAO,MAAM,iBAAiB,2JAI1B,CAAA;AAEJ,eAAO,MAAM,eAAe,gEAO8B,CAAA;AAE1D,eAAO,MAAM,gBAAgB,yOAAgD,CAAA;AAC7E,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAA;AAEzD,qBAAa,SAAS;;gBAWlB,MAAM,GAAE,UAA4C,EACpD,gBAAgB,SAAwB;IAW1C;;OAEG;IACH,SAAS,KAAK,cAAc,WAE3B;IAED,SAAS,CAAC,MAAM;IA4BhB,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM;IAO1B,IAAI;IAKJ,KAAK,CAAC,KAAK,EAAE,MAAM;CAG3B"}
|