@atproto/oauth-provider 0.5.1 → 0.6.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 +39 -0
- package/dist/account/account-manager.d.ts +7 -5
- package/dist/account/account-manager.d.ts.map +1 -1
- package/dist/account/account-manager.js +34 -25
- package/dist/account/account-manager.js.map +1 -1
- package/dist/account/account-store.d.ts +13 -5
- package/dist/account/account-store.d.ts.map +1 -1
- package/dist/account/account-store.js +24 -8
- package/dist/account/account-store.js.map +1 -1
- package/dist/account/account.d.ts +1 -11
- package/dist/account/account.d.ts.map +1 -1
- package/dist/account/{sign-up-data.d.ts → sign-up-input.d.ts} +5 -5
- package/dist/account/sign-up-input.d.ts.map +1 -0
- package/dist/account/{sign-up-data.js → sign-up-input.js} +3 -3
- package/dist/account/sign-up-input.js.map +1 -0
- package/dist/assets/assets-middleware.d.ts +2 -0
- package/dist/assets/assets-middleware.d.ts.map +1 -1
- package/dist/assets/assets-middleware.js +12 -14
- package/dist/assets/assets-middleware.js.map +1 -1
- package/dist/errors/invalid-invite-code-error.d.ts +5 -0
- package/dist/errors/invalid-invite-code-error.d.ts.map +1 -0
- package/dist/errors/invalid-invite-code-error.js +11 -0
- package/dist/errors/invalid-invite-code-error.js.map +1 -0
- package/dist/errors/oauth-error.d.ts +2 -2
- package/dist/errors/oauth-error.js.map +1 -1
- package/dist/lib/csp/index.d.ts +5 -6
- package/dist/lib/csp/index.d.ts.map +1 -1
- package/dist/lib/csp/index.js +14 -11
- package/dist/lib/csp/index.js.map +1 -1
- package/dist/lib/hcaptcha.d.ts +5 -3
- package/dist/lib/hcaptcha.d.ts.map +1 -1
- package/dist/lib/hcaptcha.js +7 -4
- package/dist/lib/hcaptcha.js.map +1 -1
- package/dist/lib/html/build-document.d.ts +2 -2
- package/dist/lib/html/build-document.d.ts.map +1 -1
- package/dist/lib/html/build-document.js +11 -7
- package/dist/lib/html/build-document.js.map +1 -1
- package/dist/lib/html/html.d.ts.map +1 -1
- package/dist/lib/html/html.js +10 -13
- package/dist/lib/html/html.js.map +1 -1
- package/dist/lib/html/util.d.ts +0 -1
- package/dist/lib/html/util.d.ts.map +1 -1
- package/dist/lib/html/util.js +0 -4
- package/dist/lib/html/util.js.map +1 -1
- package/dist/lib/http/response.d.ts +3 -1
- package/dist/lib/http/response.d.ts.map +1 -1
- package/dist/lib/http/response.js +3 -0
- package/dist/lib/http/response.js.map +1 -1
- package/dist/lib/http/security-headers.d.ts +48 -0
- package/dist/lib/http/security-headers.d.ts.map +1 -0
- package/dist/lib/http/security-headers.js +62 -0
- package/dist/lib/http/security-headers.js.map +1 -0
- package/dist/lib/util/type.d.ts +8 -0
- package/dist/lib/util/type.d.ts.map +1 -1
- package/dist/lib/util/type.js.map +1 -1
- package/dist/oauth-errors.d.ts +1 -0
- package/dist/oauth-errors.d.ts.map +1 -1
- package/dist/oauth-errors.js +3 -1
- package/dist/oauth-errors.js.map +1 -1
- package/dist/oauth-hooks.d.ts +4 -25
- package/dist/oauth-hooks.d.ts.map +1 -1
- package/dist/oauth-provider.d.ts.map +1 -1
- package/dist/oauth-provider.js +26 -25
- package/dist/oauth-provider.js.map +1 -1
- package/dist/output/backend-data.d.ts +4 -0
- package/dist/output/backend-data.d.ts.map +1 -0
- package/dist/output/backend-data.js +19 -0
- package/dist/output/backend-data.js.map +1 -0
- package/dist/output/build-authorize-data.d.ts +3 -19
- package/dist/output/build-authorize-data.d.ts.map +1 -1
- package/dist/output/build-authorize-data.js.map +1 -1
- package/dist/output/build-customization-data.d.ts +11 -18
- package/dist/output/build-customization-data.d.ts.map +1 -1
- package/dist/output/build-customization-data.js +1 -1
- package/dist/output/build-customization-data.js.map +1 -1
- package/dist/output/build-error-data.d.ts +3 -0
- package/dist/output/build-error-data.d.ts.map +1 -0
- package/dist/output/build-error-data.js +10 -0
- package/dist/output/build-error-data.js.map +1 -0
- package/dist/output/build-error-payload.d.ts +2 -1
- package/dist/output/build-error-payload.d.ts.map +1 -1
- package/dist/output/build-error-payload.js.map +1 -1
- package/dist/output/output-manager.d.ts +10 -4
- package/dist/output/output-manager.d.ts.map +1 -1
- package/dist/output/output-manager.js +68 -39
- package/dist/output/output-manager.js.map +1 -1
- package/dist/output/send-web-page.d.ts +6 -10
- package/dist/output/send-web-page.d.ts.map +1 -1
- package/dist/output/send-web-page.js +27 -47
- package/dist/output/send-web-page.js.map +1 -1
- package/dist/signer/signed-token-payload.d.ts +3 -3
- package/dist/signer/signer.d.ts +2 -2
- package/package.json +7 -39
- package/src/account/account-manager.ts +55 -34
- package/src/account/account-store.ts +29 -6
- package/src/account/account.ts +1 -14
- package/src/account/{sign-up-data.ts → sign-up-input.ts} +2 -2
- package/src/assets/assets-middleware.ts +11 -17
- package/src/errors/invalid-invite-code-error.ts +10 -0
- package/src/errors/oauth-error.ts +1 -1
- package/src/lib/csp/index.ts +16 -13
- package/src/lib/hcaptcha.ts +10 -7
- package/src/lib/html/build-document.ts +15 -8
- package/src/lib/html/html.ts +11 -18
- package/src/lib/html/util.ts +0 -4
- package/src/lib/http/response.ts +9 -1
- package/src/lib/http/security-headers.ts +91 -0
- package/src/lib/util/type.ts +18 -0
- package/src/oauth-errors.ts +1 -0
- package/src/oauth-hooks.ts +4 -25
- package/src/oauth-provider.ts +40 -34
- package/src/output/backend-data.ts +18 -0
- package/src/output/build-authorize-data.ts +3 -26
- package/src/output/build-customization-data.ts +2 -13
- package/src/output/build-error-data.ts +8 -0
- package/src/output/build-error-payload.ts +4 -2
- package/src/output/output-manager.ts +86 -47
- package/src/output/send-web-page.ts +29 -58
- package/tsconfig.backend.json +1 -2
- package/tsconfig.backend.tsbuildinfo +1 -1
- package/tsconfig.json +1 -5
- package/.linguirc +0 -57
- package/dist/account/sign-up-data.d.ts.map +0 -1
- package/dist/account/sign-up-data.js.map +0 -1
- package/dist/assets/app/bundle-manifest.json +0 -614
- package/dist/assets/app/index-ItwwtJ8r.js +0 -36
- package/dist/assets/app/index-ItwwtJ8r.js.map +0 -1
- package/dist/assets/app/main-B_dNxQo_.js +0 -4
- package/dist/assets/app/main-B_dNxQo_.js.map +0 -1
- package/dist/assets/app/main-CSatvmRR.css +0 -3
- package/dist/assets/app/main-CSatvmRR.js +0 -306
- package/dist/assets/app/main-CSatvmRR.js.map +0 -1
- package/dist/assets/app/messages-BQeltXSF.js +0 -4
- package/dist/assets/app/messages-BQeltXSF.js.map +0 -1
- package/dist/assets/app/messages-BQkEhfjg.js +0 -4
- package/dist/assets/app/messages-BQkEhfjg.js.map +0 -1
- package/dist/assets/app/messages-BUjKj_UJ.js +0 -4
- package/dist/assets/app/messages-BUjKj_UJ.js.map +0 -1
- package/dist/assets/app/messages-BWIQa8fO.js +0 -4
- package/dist/assets/app/messages-BWIQa8fO.js.map +0 -1
- package/dist/assets/app/messages-BaNVb0bp.js +0 -4
- package/dist/assets/app/messages-BaNVb0bp.js.map +0 -1
- package/dist/assets/app/messages-BaizVXcF.js +0 -4
- package/dist/assets/app/messages-BaizVXcF.js.map +0 -1
- package/dist/assets/app/messages-BfoClA1Y.js +0 -4
- package/dist/assets/app/messages-BfoClA1Y.js.map +0 -1
- package/dist/assets/app/messages-BsKGDZnC.js +0 -4
- package/dist/assets/app/messages-BsKGDZnC.js.map +0 -1
- package/dist/assets/app/messages-Bu-TJhml.js +0 -4
- package/dist/assets/app/messages-Bu-TJhml.js.map +0 -1
- package/dist/assets/app/messages-BvOKnBQk.js +0 -4
- package/dist/assets/app/messages-BvOKnBQk.js.map +0 -1
- package/dist/assets/app/messages-BxDzCiWz.js +0 -4
- package/dist/assets/app/messages-BxDzCiWz.js.map +0 -1
- package/dist/assets/app/messages-CDgFOy4S.js +0 -4
- package/dist/assets/app/messages-CDgFOy4S.js.map +0 -1
- package/dist/assets/app/messages-CLbTz0o9.js +0 -4
- package/dist/assets/app/messages-CLbTz0o9.js.map +0 -1
- package/dist/assets/app/messages-CNwSh0t7.js +0 -4
- package/dist/assets/app/messages-CNwSh0t7.js.map +0 -1
- package/dist/assets/app/messages-CSMNJ6P8.js +0 -4
- package/dist/assets/app/messages-CSMNJ6P8.js.map +0 -1
- package/dist/assets/app/messages-CZQUw3mp.js +0 -4
- package/dist/assets/app/messages-CZQUw3mp.js.map +0 -1
- package/dist/assets/app/messages-CZT41oVp.js +0 -4
- package/dist/assets/app/messages-CZT41oVp.js.map +0 -1
- package/dist/assets/app/messages-C_b-d3t8.js +0 -4
- package/dist/assets/app/messages-C_b-d3t8.js.map +0 -1
- package/dist/assets/app/messages-C_u3MTc2.js +0 -4
- package/dist/assets/app/messages-C_u3MTc2.js.map +0 -1
- package/dist/assets/app/messages-Cn8nHZic.js +0 -4
- package/dist/assets/app/messages-Cn8nHZic.js.map +0 -1
- package/dist/assets/app/messages-CtDywJUm.js +0 -4
- package/dist/assets/app/messages-CtDywJUm.js.map +0 -1
- package/dist/assets/app/messages-CurtIjBF.js +0 -4
- package/dist/assets/app/messages-CurtIjBF.js.map +0 -1
- package/dist/assets/app/messages-Cv6zIbaP.js +0 -4
- package/dist/assets/app/messages-Cv6zIbaP.js.map +0 -1
- package/dist/assets/app/messages-D1eLQuPE.js +0 -4
- package/dist/assets/app/messages-D1eLQuPE.js.map +0 -1
- package/dist/assets/app/messages-D8vHEaYW.js +0 -4
- package/dist/assets/app/messages-D8vHEaYW.js.map +0 -1
- package/dist/assets/app/messages-DJ1Q4GeC.js +0 -4
- package/dist/assets/app/messages-DJ1Q4GeC.js.map +0 -1
- package/dist/assets/app/messages-DRL3exqd.js +0 -4
- package/dist/assets/app/messages-DRL3exqd.js.map +0 -1
- package/dist/assets/app/messages-DWLPQRTp.js +0 -4
- package/dist/assets/app/messages-DWLPQRTp.js.map +0 -1
- package/dist/assets/app/messages-DjVaE9YE.js +0 -4
- package/dist/assets/app/messages-DjVaE9YE.js.map +0 -1
- package/dist/assets/app/messages-DqpMfFJR.js +0 -4
- package/dist/assets/app/messages-DqpMfFJR.js.map +0 -1
- package/dist/assets/app/messages-ETjhJBEN.js +0 -4
- package/dist/assets/app/messages-ETjhJBEN.js.map +0 -1
- package/dist/assets/app/messages-EUKrgrGn.js +0 -4
- package/dist/assets/app/messages-EUKrgrGn.js.map +0 -1
- package/dist/assets/app/messages-QQrOUcPW.js +0 -4
- package/dist/assets/app/messages-QQrOUcPW.js.map +0 -1
- package/dist/assets/app/messages-e2QGqFL6.js +0 -4
- package/dist/assets/app/messages-e2QGqFL6.js.map +0 -1
- package/dist/assets/app/messages-p61py7gD.js +0 -4
- package/dist/assets/app/messages-p61py7gD.js.map +0 -1
- package/dist/assets/asset.d.ts +0 -9
- package/dist/assets/asset.d.ts.map +0 -1
- package/dist/assets/asset.js +0 -3
- package/dist/assets/asset.js.map +0 -1
- package/dist/assets/index.d.ts +0 -5
- package/dist/assets/index.d.ts.map +0 -1
- package/dist/assets/index.js +0 -78
- package/dist/assets/index.js.map +0 -1
- package/rollup.config.js +0 -98
- package/src/assets/app/app.tsx +0 -43
- package/src/assets/app/backend-data.ts +0 -27
- package/src/assets/app/backend-types.ts +0 -66
- package/src/assets/app/components/forms/button-toggle-visibility.tsx +0 -43
- package/src/assets/app/components/forms/button.tsx +0 -60
- package/src/assets/app/components/forms/fieldset.tsx +0 -55
- package/src/assets/app/components/forms/form-card-async.tsx +0 -103
- package/src/assets/app/components/forms/form-card.tsx +0 -49
- package/src/assets/app/components/forms/input-checkbox.tsx +0 -73
- package/src/assets/app/components/forms/input-container.tsx +0 -107
- package/src/assets/app/components/forms/input-email-address.tsx +0 -66
- package/src/assets/app/components/forms/input-new-password.tsx +0 -62
- package/src/assets/app/components/forms/input-password.tsx +0 -88
- package/src/assets/app/components/forms/input-text.tsx +0 -76
- package/src/assets/app/components/forms/input-token.tsx +0 -94
- package/src/assets/app/components/forms/wizard-card.tsx +0 -116
- package/src/assets/app/components/layouts/layout-title-page.tsx +0 -77
- package/src/assets/app/components/layouts/layout-welcome.tsx +0 -73
- package/src/assets/app/components/utils/account-identifier.tsx +0 -23
- package/src/assets/app/components/utils/account-image.tsx +0 -33
- package/src/assets/app/components/utils/admonition.tsx +0 -52
- package/src/assets/app/components/utils/client-name.tsx +0 -45
- package/src/assets/app/components/utils/error-card.tsx +0 -93
- package/src/assets/app/components/utils/error-message.tsx +0 -62
- package/src/assets/app/components/utils/help-card.tsx +0 -46
- package/src/assets/app/components/utils/icons.tsx +0 -88
- package/src/assets/app/components/utils/link-anchor.tsx +0 -28
- package/src/assets/app/components/utils/link-title.tsx +0 -26
- package/src/assets/app/components/utils/multi-lang-string.tsx +0 -56
- package/src/assets/app/components/utils/password-strength-label.tsx +0 -37
- package/src/assets/app/components/utils/password-strength-meter.tsx +0 -58
- package/src/assets/app/components/utils/url-viewer.tsx +0 -73
- package/src/assets/app/cookies.ts +0 -11
- package/src/assets/app/hooks/use-api.ts +0 -178
- package/src/assets/app/hooks/use-async-action.ts +0 -120
- package/src/assets/app/hooks/use-bound-dispatch.ts +0 -5
- package/src/assets/app/hooks/use-browser-color-scheme.ts +0 -31
- package/src/assets/app/hooks/use-csrf-token.ts +0 -5
- package/src/assets/app/hooks/use-random-string.ts +0 -37
- package/src/assets/app/hooks/use-stepper.ts +0 -87
- package/src/assets/app/index.html +0 -182
- package/src/assets/app/lib/api.ts +0 -267
- package/src/assets/app/lib/clsx.ts +0 -6
- package/src/assets/app/lib/json-client.ts +0 -94
- package/src/assets/app/lib/password.ts +0 -98
- package/src/assets/app/lib/ref.ts +0 -17
- package/src/assets/app/lib/util.ts +0 -13
- package/src/assets/app/locales/an/messages.po +0 -492
- package/src/assets/app/locales/ast/messages.po +0 -492
- package/src/assets/app/locales/ca/messages.po +0 -492
- package/src/assets/app/locales/da/messages.po +0 -492
- package/src/assets/app/locales/de/messages.po +0 -492
- package/src/assets/app/locales/el/messages.po +0 -492
- package/src/assets/app/locales/en/messages.po +0 -492
- package/src/assets/app/locales/en-GB/messages.po +0 -492
- package/src/assets/app/locales/es/messages.po +0 -492
- package/src/assets/app/locales/eu/messages.po +0 -492
- package/src/assets/app/locales/fi/messages.po +0 -492
- package/src/assets/app/locales/fr/messages.po +0 -492
- package/src/assets/app/locales/ga/messages.po +0 -492
- package/src/assets/app/locales/gl/messages.po +0 -492
- package/src/assets/app/locales/hi/messages.po +0 -492
- package/src/assets/app/locales/hu/messages.po +0 -492
- package/src/assets/app/locales/ia/messages.po +0 -492
- package/src/assets/app/locales/id/messages.po +0 -492
- package/src/assets/app/locales/it/messages.po +0 -492
- package/src/assets/app/locales/ja/messages.po +0 -492
- package/src/assets/app/locales/km/messages.po +0 -492
- package/src/assets/app/locales/ko/messages.po +0 -492
- package/src/assets/app/locales/load.ts +0 -8
- package/src/assets/app/locales/locale-context.ts +0 -19
- package/src/assets/app/locales/locale-provider.tsx +0 -112
- package/src/assets/app/locales/locale-selector.tsx +0 -58
- package/src/assets/app/locales/locales.ts +0 -168
- package/src/assets/app/locales/ne/messages.po +0 -492
- package/src/assets/app/locales/nl/messages.po +0 -492
- package/src/assets/app/locales/pl/messages.po +0 -492
- package/src/assets/app/locales/pt-BR/messages.po +0 -492
- package/src/assets/app/locales/ro/messages.po +0 -492
- package/src/assets/app/locales/ru/messages.po +0 -492
- package/src/assets/app/locales/sv/messages.po +0 -492
- package/src/assets/app/locales/th/messages.po +0 -492
- package/src/assets/app/locales/tr/messages.po +0 -492
- package/src/assets/app/locales/uk/messages.po +0 -492
- package/src/assets/app/locales/vi/messages.po +0 -492
- package/src/assets/app/locales/zh-CN/messages.po +0 -492
- package/src/assets/app/locales/zh-HK/messages.po +0 -492
- package/src/assets/app/locales/zh-TW/messages.po +0 -492
- package/src/assets/app/main.css +0 -33
- package/src/assets/app/main.tsx +0 -44
- package/src/assets/app/views/authorize/accept/accept-form.tsx +0 -150
- package/src/assets/app/views/authorize/accept/accept-view.tsx +0 -70
- package/src/assets/app/views/authorize/authorize-view.tsx +0 -180
- package/src/assets/app/views/authorize/reset-password/reset-password-confirm-form.tsx +0 -88
- package/src/assets/app/views/authorize/reset-password/reset-password-request-form.tsx +0 -80
- package/src/assets/app/views/authorize/reset-password/reset-password-view.tsx +0 -127
- package/src/assets/app/views/authorize/sign-in/sign-in-form.tsx +0 -244
- package/src/assets/app/views/authorize/sign-in/sign-in-picker.tsx +0 -116
- package/src/assets/app/views/authorize/sign-in/sign-in-view.tsx +0 -145
- package/src/assets/app/views/authorize/sign-up/sign-up-account-form.tsx +0 -140
- package/src/assets/app/views/authorize/sign-up/sign-up-disclaimer.tsx +0 -51
- package/src/assets/app/views/authorize/sign-up/sign-up-handle-form.tsx +0 -289
- package/src/assets/app/views/authorize/sign-up/sign-up-hcaptcha-form.tsx +0 -108
- package/src/assets/app/views/authorize/sign-up/sign-up-view.tsx +0 -158
- package/src/assets/app/views/authorize/welcome/welcome-view.tsx +0 -56
- package/src/assets/app/views/error/error-view.tsx +0 -31
- package/src/assets/asset.ts +0 -9
- package/src/assets/index.ts +0 -86
- package/tailwind.config.js +0 -31
- package/tsconfig.frontend.json +0 -11
- package/tsconfig.frontend.tsbuildinfo +0 -1
- package/tsconfig.tools.json +0 -8
- package/tsconfig.tools.tsbuildinfo +0 -1
- package/vite.config.mjs +0 -16
@@ -18,9 +18,10 @@ import {
|
|
18
18
|
AccountStore,
|
19
19
|
ResetPasswordConfirmData,
|
20
20
|
ResetPasswordRequestData,
|
21
|
+
SignUpData,
|
21
22
|
} from './account-store.js'
|
22
23
|
import { SignInData } from './sign-in-data.js'
|
23
|
-
import {
|
24
|
+
import { SignUpInput } from './sign-up-input.js'
|
24
25
|
|
25
26
|
const TIMING_ATTACK_MITIGATION_DELAY = 400
|
26
27
|
const BRUTE_FORCE_MITIGATION_DELAY = 300
|
@@ -41,59 +42,79 @@ export class AccountManager {
|
|
41
42
|
: undefined
|
42
43
|
}
|
43
44
|
|
44
|
-
protected async
|
45
|
-
|
45
|
+
protected async processHcaptchaToken(
|
46
|
+
input: SignUpInput,
|
46
47
|
deviceId: DeviceId,
|
47
48
|
deviceMetadata: RequestMetadata,
|
48
|
-
): Promise<
|
49
|
-
|
50
|
-
|
51
|
-
if (this.inviteCodeRequired && !data.inviteCode) {
|
52
|
-
throw new InvalidRequestError('Invite code is required')
|
49
|
+
): Promise<HcaptchaVerifyResult | undefined> {
|
50
|
+
if (!this.hcaptchaClient) {
|
51
|
+
return undefined
|
53
52
|
}
|
54
53
|
|
55
|
-
if (
|
56
|
-
|
57
|
-
|
58
|
-
}
|
54
|
+
if (!input.hcaptchaToken) {
|
55
|
+
throw new InvalidRequestError('hCaptcha token is required')
|
56
|
+
}
|
59
57
|
|
60
|
-
|
58
|
+
const { allowed, result } = await this.hcaptchaClient
|
59
|
+
.verify(
|
61
60
|
'signup',
|
62
|
-
|
61
|
+
input.hcaptchaToken,
|
63
62
|
deviceMetadata.ipAddress,
|
64
|
-
|
63
|
+
input.handle,
|
65
64
|
deviceMetadata.userAgent,
|
66
65
|
)
|
67
|
-
|
68
|
-
|
69
|
-
data,
|
70
|
-
allowed,
|
71
|
-
result,
|
72
|
-
deviceId,
|
73
|
-
deviceMetadata,
|
66
|
+
.catch((err) => {
|
67
|
+
throw InvalidRequestError.from(err, 'hCaptcha verification failed')
|
74
68
|
})
|
75
69
|
|
76
|
-
|
77
|
-
|
78
|
-
|
70
|
+
if (!allowed) {
|
71
|
+
throw new InvalidRequestError('hCaptcha verification failed')
|
72
|
+
}
|
73
|
+
|
74
|
+
return result
|
75
|
+
}
|
79
76
|
|
80
|
-
|
77
|
+
protected async enforceInviteCode(
|
78
|
+
input: SignUpInput,
|
79
|
+
_deviceId: DeviceId,
|
80
|
+
_deviceMetadata: RequestMetadata,
|
81
|
+
): Promise<string | undefined> {
|
82
|
+
if (!this.inviteCodeRequired) {
|
83
|
+
return undefined
|
81
84
|
}
|
82
85
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
86
|
+
if (!input.inviteCode) {
|
87
|
+
throw new InvalidRequestError('Invite code is required')
|
88
|
+
}
|
89
|
+
|
90
|
+
return input.inviteCode
|
91
|
+
}
|
92
|
+
|
93
|
+
protected async buildSignupData(
|
94
|
+
input: SignUpInput,
|
95
|
+
deviceId: DeviceId,
|
96
|
+
deviceMetadata: RequestMetadata,
|
97
|
+
): Promise<SignUpData> {
|
98
|
+
const [hcaptchaResult, inviteCode] = await Promise.all([
|
99
|
+
this.processHcaptchaToken(input, deviceId, deviceMetadata),
|
100
|
+
this.enforceInviteCode(input, deviceId, deviceMetadata),
|
101
|
+
])
|
102
|
+
|
103
|
+
return { ...input, hcaptchaResult, inviteCode }
|
89
104
|
}
|
90
105
|
|
91
106
|
public async signUp(
|
92
|
-
|
107
|
+
input: SignUpInput,
|
93
108
|
deviceId: DeviceId,
|
94
109
|
deviceMetadata: RequestMetadata,
|
95
110
|
): Promise<AccountInfo> {
|
96
|
-
await this.
|
111
|
+
await callAsync(this.hooks.onSignupAttempt, {
|
112
|
+
input,
|
113
|
+
deviceId,
|
114
|
+
deviceMetadata,
|
115
|
+
})
|
116
|
+
|
117
|
+
const data = await this.buildSignupData(input, deviceId, deviceMetadata)
|
97
118
|
|
98
119
|
// Mitigation against brute forcing email of users.
|
99
120
|
// @TODO Add rate limit to all the OAuth routes.
|
@@ -1,8 +1,10 @@
|
|
1
1
|
import { isEmailValid } from '@hapi/address'
|
2
2
|
import { isDisposableEmail } from 'disposable-email-domains-js'
|
3
3
|
import { z } from 'zod'
|
4
|
+
import { ensureValidHandle, normalizeHandle } from '@atproto/syntax'
|
4
5
|
import { ClientId } from '../client/client-id.js'
|
5
6
|
import { DeviceId } from '../device/device-id.js'
|
7
|
+
import { HcaptchaVerifyResult } from '../lib/hcaptcha.js'
|
6
8
|
import { localeSchema } from '../lib/locale.js'
|
7
9
|
import { Awaitable, buildInterfaceChecker } from '../lib/util/type.js'
|
8
10
|
import {
|
@@ -12,16 +14,29 @@ import {
|
|
12
14
|
} from '../oauth-errors.js'
|
13
15
|
import { Sub } from '../oidc/sub.js'
|
14
16
|
import { Account } from './account.js'
|
17
|
+
import { SignUpInput } from './sign-up-input.js'
|
15
18
|
|
16
19
|
// @NOTE Change the length here to force stronger passwords (through a reset)
|
17
20
|
export const oldPasswordSchema = z.string().min(1)
|
18
21
|
export const newPasswordSchema = z.string().min(8)
|
19
|
-
export const tokenSchema = z
|
22
|
+
export const tokenSchema = z
|
23
|
+
.string()
|
24
|
+
.regex(/^[A-Z2-7]{5}-[A-Z2-7]{5}$/, 'Invalid token format')
|
20
25
|
export const handleSchema = z
|
21
26
|
.string()
|
22
|
-
.
|
23
|
-
.
|
24
|
-
.
|
27
|
+
// @NOTE: We only check against validity towards ATProto's syntax. Additional
|
28
|
+
// rules may be imposed by the store implementation.
|
29
|
+
.superRefine((value, ctx) => {
|
30
|
+
try {
|
31
|
+
ensureValidHandle(value)
|
32
|
+
} catch (err) {
|
33
|
+
ctx.addIssue({
|
34
|
+
code: z.ZodIssueCode.custom,
|
35
|
+
message: err instanceof Error ? err.message : 'Invalid handle',
|
36
|
+
})
|
37
|
+
}
|
38
|
+
})
|
39
|
+
.transform(normalizeHandle)
|
25
40
|
export const emailSchema = z
|
26
41
|
.string()
|
27
42
|
.email()
|
@@ -34,13 +49,16 @@ export const emailSchema = z
|
|
34
49
|
.refine((email) => !isDisposableEmail(email), {
|
35
50
|
message: 'Disposable email addresses are not allowed',
|
36
51
|
})
|
52
|
+
.transform((value) => value.toLowerCase())
|
53
|
+
export const inviteCodeSchema = z.string().min(1)
|
54
|
+
export type InviteCode = z.infer<typeof inviteCodeSchema>
|
37
55
|
|
38
56
|
export const authenticateAccountDataSchema = z
|
39
57
|
.object({
|
40
58
|
locale: localeSchema,
|
41
59
|
username: z.string(),
|
42
60
|
password: oldPasswordSchema,
|
43
|
-
emailOtp:
|
61
|
+
emailOtp: tokenSchema.optional(),
|
44
62
|
})
|
45
63
|
.strict()
|
46
64
|
|
@@ -54,7 +72,7 @@ export const createAccountDataSchema = z
|
|
54
72
|
handle: handleSchema,
|
55
73
|
email: emailSchema,
|
56
74
|
password: z.intersection(oldPasswordSchema, newPasswordSchema),
|
57
|
-
inviteCode:
|
75
|
+
inviteCode: inviteCodeSchema.optional(),
|
58
76
|
})
|
59
77
|
.strict()
|
60
78
|
|
@@ -103,6 +121,11 @@ export type AccountInfo = {
|
|
103
121
|
info: DeviceAccountInfo
|
104
122
|
}
|
105
123
|
|
124
|
+
export type SignUpData = SignUpInput & {
|
125
|
+
hcaptchaResult?: HcaptchaVerifyResult
|
126
|
+
inviteCode?: InviteCode
|
127
|
+
}
|
128
|
+
|
106
129
|
export interface AccountStore {
|
107
130
|
/**
|
108
131
|
* @throws {HandleUnavailableError} - To indicate that the handle is already taken
|
package/src/account/account.ts
CHANGED
@@ -1,14 +1 @@
|
|
1
|
-
|
2
|
-
import { Sub } from '../oidc/sub.js'
|
3
|
-
|
4
|
-
export type Account = Simplify<{
|
5
|
-
sub: Sub // Account id
|
6
|
-
aud: string | [string, ...string[]] // Resource server URL
|
7
|
-
|
8
|
-
// OIDC inspired
|
9
|
-
preferred_username?: string
|
10
|
-
email?: string
|
11
|
-
email_verified?: boolean
|
12
|
-
picture?: string
|
13
|
-
name?: string
|
14
|
-
}>
|
1
|
+
export type { Account } from '@atproto/oauth-provider-api'
|
@@ -2,10 +2,10 @@ import { z } from 'zod'
|
|
2
2
|
import { hcaptchaTokenSchema } from '../lib/hcaptcha.js'
|
3
3
|
import { createAccountDataSchema } from './account-store.js'
|
4
4
|
|
5
|
-
export const
|
5
|
+
export const signUpInputSchema = createAccountDataSchema
|
6
6
|
.extend({
|
7
7
|
hcaptchaToken: hcaptchaTokenSchema.optional(),
|
8
8
|
})
|
9
9
|
.strict()
|
10
10
|
|
11
|
-
export type
|
11
|
+
export type SignUpInput = z.TypeOf<typeof signUpInputSchema>
|
@@ -1,33 +1,27 @@
|
|
1
|
+
import { assets } from '@atproto/oauth-provider-ui'
|
1
2
|
import {
|
2
3
|
Middleware,
|
3
4
|
validateFetchDest,
|
4
5
|
validateFetchSite,
|
5
6
|
writeStream,
|
6
7
|
} from '../lib/http/index.js'
|
7
|
-
|
8
|
-
|
8
|
+
|
9
|
+
export const ASSETS_URL_PREFIX = '/@atproto/oauth-provider/~assets/'
|
10
|
+
|
11
|
+
export function buildAssetUrl(filename: string): string {
|
12
|
+
return `${ASSETS_URL_PREFIX}${encodeURIComponent(filename)}`
|
13
|
+
}
|
9
14
|
|
10
15
|
export function authorizeAssetsMiddleware(): Middleware {
|
11
16
|
return async function assetsMiddleware(req, res, next): Promise<void> {
|
12
17
|
if (req.method !== 'GET' && req.method !== 'HEAD') return next()
|
13
18
|
if (!req.url?.startsWith(ASSETS_URL_PREFIX)) return next()
|
14
19
|
|
15
|
-
const
|
16
|
-
string,
|
17
|
-
string | undefined,
|
18
|
-
]
|
19
|
-
if (query) return next()
|
20
|
-
|
21
|
-
const filename = pathname.slice(ASSETS_URL_PREFIX.length)
|
20
|
+
const filename = req.url.slice(ASSETS_URL_PREFIX.length)
|
22
21
|
if (!filename) return next()
|
23
22
|
|
24
|
-
|
25
|
-
|
26
|
-
asset = getAsset(filename)
|
27
|
-
} catch {
|
28
|
-
// Filename not found or not valid
|
29
|
-
return next()
|
30
|
-
}
|
23
|
+
const asset = assets.get(filename)
|
24
|
+
if (!asset) return next()
|
31
25
|
|
32
26
|
try {
|
33
27
|
// Allow "null" (ie. no header) to allow loading assets outside of a
|
@@ -45,6 +39,6 @@ export function authorizeAssetsMiddleware(): Middleware {
|
|
45
39
|
res.setHeader('ETag', asset.sha256)
|
46
40
|
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
|
47
41
|
|
48
|
-
writeStream(res, asset.
|
42
|
+
writeStream(res, asset.stream(), { contentType: asset.mime })
|
49
43
|
}
|
50
44
|
}
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import { InvalidRequestError } from './invalid-request-error'
|
2
|
+
|
3
|
+
export class InvalidInviteCodeError extends InvalidRequestError {
|
4
|
+
constructor(details?: string, cause?: unknown) {
|
5
|
+
super(
|
6
|
+
'This invite code is invalid.' + (details ? ` ${details}` : ''),
|
7
|
+
cause,
|
8
|
+
)
|
9
|
+
}
|
10
|
+
}
|
package/src/lib/csp/index.ts
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
-
import { Simplify } from '../util/type.js'
|
1
|
+
import { CombinedTuple, Simplify } from '../util/type.js'
|
2
2
|
|
3
3
|
export type CspValue =
|
4
4
|
| `data:`
|
5
|
+
| `http:${string}`
|
5
6
|
| `https:${string}`
|
6
7
|
| `'none'`
|
7
8
|
| `'self'`
|
@@ -35,7 +36,7 @@ export type CspConfig = Simplify<
|
|
35
36
|
} & {
|
36
37
|
[K in (typeof STRING_DIRECTIVES)[number]]?: CspValue
|
37
38
|
} & {
|
38
|
-
[K in (typeof ARRAY_DIRECTIVES)[number]]?:
|
39
|
+
[K in (typeof ARRAY_DIRECTIVES)[number]]?: Iterable<CspValue>
|
39
40
|
}
|
40
41
|
>
|
41
42
|
|
@@ -53,25 +54,27 @@ export function buildCsp(config: CspConfig): string {
|
|
53
54
|
}
|
54
55
|
|
55
56
|
for (const name of ARRAY_DIRECTIVES) {
|
56
|
-
|
57
|
+
// Remove duplicate values by using a Set
|
58
|
+
const val = config[name] ? new Set(config[name]) : undefined
|
59
|
+
if (val?.size) values.push(`${name} ${Array.from(val).join(' ')}`)
|
57
60
|
}
|
58
61
|
|
59
62
|
return values.join('; ')
|
60
63
|
}
|
61
64
|
|
62
|
-
export function mergeCsp(
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
if (!b) return a
|
65
|
+
export function mergeCsp<C extends (CspConfig | null | undefined)[]>(
|
66
|
+
...configs: C
|
67
|
+
) {
|
68
|
+
return configs.filter((v) => v != null).reduce(combineCsp) as CombinedTuple<C>
|
69
|
+
}
|
68
70
|
|
71
|
+
export function combineCsp(a: CspConfig, b: CspConfig): CspConfig {
|
69
72
|
const result: CspConfig = {}
|
70
73
|
|
71
74
|
for (const name of BOOLEAN_DIRECTIVES) {
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
+
// @NOTE b (if defined) takes precedence
|
76
|
+
const value = b[name] ?? a[name]
|
77
|
+
if (value != null) result[name] = value
|
75
78
|
}
|
76
79
|
|
77
80
|
for (const name of STRING_DIRECTIVES) {
|
@@ -90,7 +93,7 @@ export function mergeCsp(a?: CspConfig, b?: CspConfig): CspConfig | undefined {
|
|
90
93
|
if (set.size > 1 && set.has(NONE)) set.delete(NONE)
|
91
94
|
result[name] = [...set]
|
92
95
|
} else if (a[name] || b[name]) {
|
93
|
-
result[name] =
|
96
|
+
result[name] = a[name] || b[name]
|
94
97
|
}
|
95
98
|
}
|
96
99
|
|
package/src/lib/hcaptcha.ts
CHANGED
@@ -27,9 +27,11 @@ export const hcaptchaConfigSchema = z.object({
|
|
27
27
|
*/
|
28
28
|
tokenSalt: z.string().min(1),
|
29
29
|
/**
|
30
|
-
* The risk score
|
30
|
+
* The risk score above which the user is considered a threat and will be
|
31
31
|
* denied access. This will be ignored if the enterprise features are not
|
32
32
|
* available.
|
33
|
+
*
|
34
|
+
* Note: Score values ranges from 0.0 (no risk) to 1.0 (confirmed threat).
|
33
35
|
*/
|
34
36
|
scoreThreshold: z.number().optional(),
|
35
37
|
})
|
@@ -128,7 +130,7 @@ export class HCaptchaClient {
|
|
128
130
|
this.fetch = bindFetch(fetch)
|
129
131
|
}
|
130
132
|
|
131
|
-
async verify(
|
133
|
+
public async verify(
|
132
134
|
behaviorType: 'login' | 'signup',
|
133
135
|
response: string,
|
134
136
|
remoteip: string,
|
@@ -160,20 +162,21 @@ export class HCaptchaClient {
|
|
160
162
|
}
|
161
163
|
}
|
162
164
|
|
163
|
-
isAllowed({ success, hostname, score }: HcaptchaVerifyResult) {
|
165
|
+
protected isAllowed({ success, hostname, score }: HcaptchaVerifyResult) {
|
164
166
|
return (
|
165
167
|
success &&
|
166
168
|
// Fool-proofing: If this is false, the user is trying to use a token
|
167
169
|
// generated for the same siteKey, but on another domain.
|
168
170
|
hostname === this.hostname &&
|
169
171
|
// Ignore if enterprise feature is not enabled
|
170
|
-
score
|
171
|
-
|
172
|
-
|
172
|
+
(score == null ||
|
173
|
+
// Ignore if disabled through config
|
174
|
+
this.config.scoreThreshold == null ||
|
175
|
+
score < this.config.scoreThreshold)
|
173
176
|
)
|
174
177
|
}
|
175
178
|
|
176
|
-
hashToken(value: string) {
|
179
|
+
protected hashToken(value: string) {
|
177
180
|
const hash = createHash('sha256')
|
178
181
|
hash.update(this.config.tokenSalt)
|
179
182
|
hash.update(value)
|
@@ -4,7 +4,6 @@ import { html } from './tags.js'
|
|
4
4
|
|
5
5
|
export type AssetRef = {
|
6
6
|
url: string
|
7
|
-
sha256: string
|
8
7
|
}
|
9
8
|
|
10
9
|
export type Attrs = Record<string, boolean | string | undefined>
|
@@ -59,6 +58,7 @@ export type BuildDocumentOptions = {
|
|
59
58
|
base?: URL
|
60
59
|
meta?: readonly MetaAttrs[]
|
61
60
|
links?: readonly LinkAttrs[]
|
61
|
+
preloads?: readonly AssetRef[]
|
62
62
|
head?: HtmlValue
|
63
63
|
title?: HtmlValue
|
64
64
|
scripts?: readonly (Html | AssetRef)[]
|
@@ -76,6 +76,7 @@ export const buildDocument = ({
|
|
76
76
|
base,
|
77
77
|
meta,
|
78
78
|
links,
|
79
|
+
preloads,
|
79
80
|
scripts,
|
80
81
|
styles,
|
81
82
|
}: BuildDocumentOptions) => html`<!doctype html>
|
@@ -86,8 +87,7 @@ export const buildDocument = ({
|
|
86
87
|
${base && html`<base href="${base.href}" />`}
|
87
88
|
${meta?.some(isViewportMeta) ? null : defaultViewport}
|
88
89
|
${meta?.map(metaToHtml)}
|
89
|
-
${
|
90
|
-
${scripts?.map(linkPreload('script'))}
|
90
|
+
${preloads?.map(linkPreload)}
|
91
91
|
${links?.map(linkToHtml)}
|
92
92
|
${head}
|
93
93
|
${styles?.map(styleToHtml)}
|
@@ -120,11 +120,18 @@ function* attrsToHtml(attrs?: Attrs) {
|
|
120
120
|
}
|
121
121
|
}
|
122
122
|
|
123
|
-
function linkPreload(
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
123
|
+
function linkPreload(asset: AssetRef) {
|
124
|
+
const [path] = asset.url.split('?', 2)
|
125
|
+
|
126
|
+
if (path.endsWith('.js')) {
|
127
|
+
return html`<link rel="modulepreload" href="${asset.url}" />`
|
128
|
+
}
|
129
|
+
|
130
|
+
if (path.endsWith('.css')) {
|
131
|
+
return html`<link rel="preload" href="${asset.url}" as="style" />`
|
132
|
+
}
|
133
|
+
|
134
|
+
return undefined
|
128
135
|
}
|
129
136
|
|
130
137
|
function scriptToHtml(script: Html | AssetRef) {
|
package/src/lib/html/html.ts
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
import { isString } from './util'
|
2
|
-
|
3
1
|
const symbol = Symbol('Html.dangerouslyCreate')
|
4
2
|
|
5
3
|
/**
|
@@ -7,34 +5,29 @@ const symbol = Symbol('Html.dangerouslyCreate')
|
|
7
5
|
* or used as fragments to build a larger HTML document.
|
8
6
|
*/
|
9
7
|
export class Html implements Iterable<string> {
|
10
|
-
#fragments:
|
8
|
+
readonly #fragments: readonly (Html | string)[]
|
11
9
|
|
12
10
|
private constructor(fragments: Iterable<Html | string>, guard: symbol) {
|
13
11
|
if (guard !== symbol) {
|
14
|
-
//
|
12
|
+
// Forces developers to use `Html.dangerouslyCreate` to create an Html
|
15
13
|
// instance, to make it clear that the content needs to be trusted.
|
16
14
|
throw new TypeError(
|
17
15
|
'Use Html.dangerouslyCreate() to create an Html instance',
|
18
16
|
)
|
19
17
|
}
|
20
18
|
|
21
|
-
|
19
|
+
// Transform into an array in case iterable can be consumed only once
|
20
|
+
// (e.g. a generator function).
|
21
|
+
this.#fragments = Array.from(fragments)
|
22
22
|
}
|
23
23
|
|
24
24
|
toString(): string {
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
//
|
29
|
-
|
30
|
-
|
31
|
-
this.#fragments.length > 1 ||
|
32
|
-
!this.#fragments.every(isString)
|
33
|
-
) {
|
34
|
-
this.#fragments = result ? [result] : []
|
35
|
-
}
|
36
|
-
|
37
|
-
return result
|
25
|
+
// More efficient than `return this.#fragments.join('')` because it avoids
|
26
|
+
// creating intermediate strings when items of this.#fragments are Html
|
27
|
+
// instances (as all their toString() would end-up being called, creating
|
28
|
+
// lots of intermediary strings). The approach here allows to do a full scan
|
29
|
+
// of all the child nodes and concatenate them in a single pass.
|
30
|
+
return Array.from(this).join('')
|
38
31
|
}
|
39
32
|
|
40
33
|
[Symbol.toPrimitive](hint): string {
|
package/src/lib/html/util.ts
CHANGED
package/src/lib/http/response.ts
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
import type { ServerResponse } from 'node:http'
|
2
2
|
import { type Readable, pipeline } from 'node:stream'
|
3
|
+
import {
|
4
|
+
SecurityHeadersOptions,
|
5
|
+
setSecurityHeaders,
|
6
|
+
} from './security-headers.js'
|
3
7
|
import type { Handler, Middleware } from './types.js'
|
4
8
|
|
5
9
|
export function appendHeader(
|
@@ -88,11 +92,15 @@ export function staticJsonMiddleware(
|
|
88
92
|
}
|
89
93
|
}
|
90
94
|
|
95
|
+
export type WriteHtmlOptions = WriteResponseOptions & SecurityHeadersOptions
|
96
|
+
|
91
97
|
export function writeHtml(
|
92
98
|
res: ServerResponse,
|
93
99
|
html: Buffer | string,
|
94
|
-
{ contentType = 'text/html', ...options }:
|
100
|
+
{ contentType = 'text/html', ...options }: WriteHtmlOptions = {},
|
95
101
|
): void {
|
102
|
+
// HTML pages should always be served with safety protection headers
|
103
|
+
setSecurityHeaders(res, options)
|
96
104
|
writeBuffer(res, html, { ...options, contentType })
|
97
105
|
}
|
98
106
|
|
@@ -0,0 +1,91 @@
|
|
1
|
+
import type { ServerResponse } from 'node:http'
|
2
|
+
import { type CspConfig, buildCsp } from '../csp/index.js'
|
3
|
+
|
4
|
+
/**
|
5
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy COEP on MDN}
|
6
|
+
*/
|
7
|
+
export enum CrossOriginEmbedderPolicy {
|
8
|
+
unsafeNone = 'unsafe-none',
|
9
|
+
requireCorp = 'require-corp',
|
10
|
+
credentialless = 'credentialless',
|
11
|
+
}
|
12
|
+
|
13
|
+
/**
|
14
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy CORP on MDN}
|
15
|
+
*/
|
16
|
+
export enum CrossOriginResourcePolicy {
|
17
|
+
sameSite = 'same-site',
|
18
|
+
sameOrigin = 'same-origin',
|
19
|
+
crossOrigin = 'cross-origin',
|
20
|
+
}
|
21
|
+
|
22
|
+
/**
|
23
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy COOP on MDN}
|
24
|
+
*/
|
25
|
+
export enum CrossOriginOpenerPolicy {
|
26
|
+
unsafeNone = 'unsafe-none',
|
27
|
+
sameOriginAllowPopups = 'same-origin-allow-popups',
|
28
|
+
sameOrigin = 'same-origin',
|
29
|
+
noopenerAllowPopups = 'noopener-allow-popups',
|
30
|
+
}
|
31
|
+
|
32
|
+
export type HTTPStrictTransportSecurityConfig = {
|
33
|
+
maxAge: number
|
34
|
+
includeSubDomains?: boolean
|
35
|
+
preload?: boolean
|
36
|
+
}
|
37
|
+
|
38
|
+
export type SecurityHeadersOptions = {
|
39
|
+
/**
|
40
|
+
* Defaults to `default-src: 'none'`. Use an empty object to disable CSP.
|
41
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy CSP on MDN}
|
42
|
+
*/
|
43
|
+
csp?: CspConfig
|
44
|
+
coep?: CrossOriginEmbedderPolicy
|
45
|
+
corp?: CrossOriginResourcePolicy
|
46
|
+
coop?: CrossOriginOpenerPolicy
|
47
|
+
/**
|
48
|
+
* Defaults to 2 years. Use `false` to disable HSTS.
|
49
|
+
*/
|
50
|
+
hsts?: HTTPStrictTransportSecurityConfig | false
|
51
|
+
}
|
52
|
+
|
53
|
+
export function setSecurityHeaders(
|
54
|
+
res: ServerResponse,
|
55
|
+
{
|
56
|
+
csp = { 'default-src': ["'none'"] },
|
57
|
+
coep = CrossOriginEmbedderPolicy.requireCorp,
|
58
|
+
corp = CrossOriginResourcePolicy.sameOrigin,
|
59
|
+
coop = CrossOriginOpenerPolicy.sameOrigin,
|
60
|
+
hsts = { maxAge: 63072000 },
|
61
|
+
}: SecurityHeadersOptions,
|
62
|
+
): void {
|
63
|
+
// @NOTE Never set CSP through http-equiv meta as not all directives will
|
64
|
+
// be honored. Always set it through the Content-Security-Policy header.
|
65
|
+
const cspString = buildCsp(csp)
|
66
|
+
if (cspString) {
|
67
|
+
res.setHeader('Content-Security-Policy', cspString)
|
68
|
+
}
|
69
|
+
|
70
|
+
res.setHeader('Cross-Origin-Embedder-Policy', coep)
|
71
|
+
res.setHeader('Cross-Origin-Resource-Policy', corp)
|
72
|
+
res.setHeader('Cross-Origin-Opener-Policy', coop)
|
73
|
+
|
74
|
+
if (hsts) {
|
75
|
+
res.setHeader('Strict-Transport-Security', buildHstsValue(hsts))
|
76
|
+
}
|
77
|
+
|
78
|
+
// @TODO: make these headers configurable (?)
|
79
|
+
res.setHeader('Permissions-Policy', 'otp-credentials=*, document-domain=()')
|
80
|
+
res.setHeader('Referrer-Policy', 'same-origin')
|
81
|
+
res.setHeader('X-Frame-Options', 'DENY')
|
82
|
+
res.setHeader('X-Content-Type-Options', 'nosniff')
|
83
|
+
res.setHeader('X-XSS-Protection', '0')
|
84
|
+
}
|
85
|
+
|
86
|
+
function buildHstsValue(config: HTTPStrictTransportSecurityConfig): string {
|
87
|
+
let value = `max-age=${config.maxAge}`
|
88
|
+
if (config.includeSubDomains) value += '; includeSubDomains'
|
89
|
+
if (config.preload) value += '; preload'
|
90
|
+
return value
|
91
|
+
}
|