@techelusive/nest-auth 0.0.0-stage → 0.2.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 +64 -0
- package/LICENSE +21 -0
- package/README.md +204 -2
- package/api/openapi.json +4399 -0
- package/api/techelusive-authentication.postman_collection.json +3178 -0
- package/dist/auth-options.d.ts +156 -0
- package/dist/auth-options.js +3 -0
- package/dist/auth-options.js.map +1 -0
- package/dist/auth-options.validation.d.ts +9 -0
- package/dist/auth-options.validation.js +74 -0
- package/dist/auth-options.validation.js.map +1 -0
- package/dist/auth.module.d.ts +38 -0
- package/dist/auth.module.js +173 -0
- package/dist/auth.module.js.map +1 -0
- package/dist/common/auth-error.d.ts +16 -0
- package/dist/common/auth-error.js +23 -0
- package/dist/common/auth-error.js.map +1 -0
- package/dist/common/auth-exception.filter.d.ts +24 -0
- package/dist/common/auth-exception.filter.js +66 -0
- package/dist/common/auth-exception.filter.js.map +1 -0
- package/dist/common/crypto/jwt.d.ts +89 -0
- package/dist/common/crypto/jwt.js +144 -0
- package/dist/common/crypto/jwt.js.map +1 -0
- package/dist/common/crypto/random-secrets.d.ts +78 -0
- package/dist/common/crypto/random-secrets.js +113 -0
- package/dist/common/crypto/random-secrets.js.map +1 -0
- package/dist/common/crypto/remote-public-keys.d.ts +49 -0
- package/dist/common/crypto/remote-public-keys.js +105 -0
- package/dist/common/crypto/remote-public-keys.js.map +1 -0
- package/dist/common/crypto/totp.d.ts +68 -0
- package/dist/common/crypto/totp.js +148 -0
- package/dist/common/crypto/totp.js.map +1 -0
- package/dist/common/encryption.service.d.ts +36 -0
- package/dist/common/encryption.service.js +111 -0
- package/dist/common/encryption.service.js.map +1 -0
- package/dist/common/identifiers.d.ts +48 -0
- package/dist/common/identifiers.js +89 -0
- package/dist/common/identifiers.js.map +1 -0
- package/dist/common/network-safety.d.ts +19 -0
- package/dist/common/network-safety.js +72 -0
- package/dist/common/network-safety.js.map +1 -0
- package/dist/common/rate-limiter.service.d.ts +63 -0
- package/dist/common/rate-limiter.service.js +158 -0
- package/dist/common/rate-limiter.service.js.map +1 -0
- package/dist/common/refresh-token-cookie.d.ts +78 -0
- package/dist/common/refresh-token-cookie.js +185 -0
- package/dist/common/refresh-token-cookie.js.map +1 -0
- package/dist/common/request-context.d.ts +14 -0
- package/dist/common/request-context.js +22 -0
- package/dist/common/request-context.js.map +1 -0
- package/dist/common/strict-validation.pipe.d.ts +8 -0
- package/dist/common/strict-validation.pipe.js +16 -0
- package/dist/common/strict-validation.pipe.js.map +1 -0
- package/dist/common/time.d.ts +15 -0
- package/dist/common/time.js +34 -0
- package/dist/common/time.js.map +1 -0
- package/dist/constants.d.ts +1326 -0
- package/dist/constants.js +1142 -0
- package/dist/constants.js.map +1 -0
- package/dist/database/create-auth-tables.migration.d.ts +26 -0
- package/dist/database/create-auth-tables.migration.js +291 -0
- package/dist/database/create-auth-tables.migration.js.map +1 -0
- package/dist/database/email-change-and-retention.migration.d.ts +25 -0
- package/dist/database/email-change-and-retention.migration.js +54 -0
- package/dist/database/email-change-and-retention.migration.js.map +1 -0
- package/dist/database/entities/api-key.entity.d.ts +17 -0
- package/dist/database/entities/api-key.entity.js +80 -0
- package/dist/database/entities/api-key.entity.js.map +1 -0
- package/dist/database/entities/audit-log.entity.d.ts +13 -0
- package/dist/database/entities/audit-log.entity.js +64 -0
- package/dist/database/entities/audit-log.entity.js.map +1 -0
- package/dist/database/entities/backup-code.entity.d.ts +9 -0
- package/dist/database/entities/backup-code.entity.js +52 -0
- package/dist/database/entities/backup-code.entity.js.map +1 -0
- package/dist/database/entities/index.d.ts +43 -0
- package/dist/database/entities/index.js +82 -0
- package/dist/database/entities/index.js.map +1 -0
- package/dist/database/entities/json-object.d.ts +12 -0
- package/dist/database/entities/json-object.js +3 -0
- package/dist/database/entities/json-object.js.map +1 -0
- package/dist/database/entities/link-token.entity.d.ts +15 -0
- package/dist/database/entities/link-token.entity.js +64 -0
- package/dist/database/entities/link-token.entity.js.map +1 -0
- package/dist/database/entities/one-time-code.entity.d.ts +16 -0
- package/dist/database/entities/one-time-code.entity.js +69 -0
- package/dist/database/entities/one-time-code.entity.js.map +1 -0
- package/dist/database/entities/organization-invitation.entity.d.ts +14 -0
- package/dist/database/entities/organization-invitation.entity.js +73 -0
- package/dist/database/entities/organization-invitation.entity.js.map +1 -0
- package/dist/database/entities/organization-member.entity.d.ts +9 -0
- package/dist/database/entities/organization-member.entity.js +48 -0
- package/dist/database/entities/organization-member.entity.js.map +1 -0
- package/dist/database/entities/organization.entity.d.ts +12 -0
- package/dist/database/entities/organization.entity.js +58 -0
- package/dist/database/entities/organization.entity.js.map +1 -0
- package/dist/database/entities/passkey.entity.d.ts +19 -0
- package/dist/database/entities/passkey.entity.js +78 -0
- package/dist/database/entities/passkey.entity.js.map +1 -0
- package/dist/database/entities/password.entity.d.ts +6 -0
- package/dist/database/entities/password.entity.js +36 -0
- package/dist/database/entities/password.entity.js.map +1 -0
- package/dist/database/entities/refresh-token.entity.d.ts +9 -0
- package/dist/database/entities/refresh-token.entity.js +53 -0
- package/dist/database/entities/refresh-token.entity.js.map +1 -0
- package/dist/database/entities/session.entity.d.ts +20 -0
- package/dist/database/entities/session.entity.js +87 -0
- package/dist/database/entities/session.entity.js.map +1 -0
- package/dist/database/entities/settings.entity.d.ts +7 -0
- package/dist/database/entities/settings.entity.js +41 -0
- package/dist/database/entities/settings.entity.js.map +1 -0
- package/dist/database/entities/sign-in-challenge.entity.d.ts +23 -0
- package/dist/database/entities/sign-in-challenge.entity.js +80 -0
- package/dist/database/entities/sign-in-challenge.entity.js.map +1 -0
- package/dist/database/entities/signing-key.entity.d.ts +11 -0
- package/dist/database/entities/signing-key.entity.js +54 -0
- package/dist/database/entities/signing-key.entity.js.map +1 -0
- package/dist/database/entities/social-account.entity.d.ts +13 -0
- package/dist/database/entities/social-account.entity.js +54 -0
- package/dist/database/entities/social-account.entity.js.map +1 -0
- package/dist/database/entities/two-factor-secret.entity.d.ts +10 -0
- package/dist/database/entities/two-factor-secret.entity.js +48 -0
- package/dist/database/entities/two-factor-secret.entity.js.map +1 -0
- package/dist/database/entities/user.entity.d.ts +34 -0
- package/dist/database/entities/user.entity.js +129 -0
- package/dist/database/entities/user.entity.js.map +1 -0
- package/dist/database/entities/webhook-delivery.entity.d.ts +17 -0
- package/dist/database/entities/webhook-delivery.entity.js +79 -0
- package/dist/database/entities/webhook-delivery.entity.js.map +1 -0
- package/dist/database/entities/webhook.entity.d.ts +12 -0
- package/dist/database/entities/webhook.entity.js +58 -0
- package/dist/database/entities/webhook.entity.js.map +1 -0
- package/dist/database/typeorm-row.d.ts +21 -0
- package/dist/database/typeorm-row.js +29 -0
- package/dist/database/typeorm-row.js.map +1 -0
- package/dist/features/account/account.controller.d.ts +259 -0
- package/dist/features/account/account.controller.js +372 -0
- package/dist/features/account/account.controller.js.map +1 -0
- package/dist/features/account/account.dto.d.ts +16 -0
- package/dist/features/account/account.dto.js +61 -0
- package/dist/features/account/account.dto.js.map +1 -0
- package/dist/features/account/data-export.service.d.ts +85 -0
- package/dist/features/account/data-export.service.js +125 -0
- package/dist/features/account/data-export.service.js.map +1 -0
- package/dist/features/account/health.service.d.ts +30 -0
- package/dist/features/account/health.service.js +59 -0
- package/dist/features/account/health.service.js.map +1 -0
- package/dist/features/admin/admin.controller.d.ts +382 -0
- package/dist/features/admin/admin.controller.js +623 -0
- package/dist/features/admin/admin.controller.js.map +1 -0
- package/dist/features/admin/admin.dto.d.ts +61 -0
- package/dist/features/admin/admin.dto.js +253 -0
- package/dist/features/admin/admin.dto.js.map +1 -0
- package/dist/features/admin/admin.service.d.ts +142 -0
- package/dist/features/admin/admin.service.js +227 -0
- package/dist/features/admin/admin.service.js.map +1 -0
- package/dist/features/api-keys/api-keys.service.d.ts +67 -0
- package/dist/features/api-keys/api-keys.service.js +144 -0
- package/dist/features/api-keys/api-keys.service.js.map +1 -0
- package/dist/features/email-change/email-change.controller.d.ts +38 -0
- package/dist/features/email-change/email-change.controller.js +85 -0
- package/dist/features/email-change/email-change.controller.js.map +1 -0
- package/dist/features/email-change/email-change.dto.d.ts +10 -0
- package/dist/features/email-change/email-change.dto.js +43 -0
- package/dist/features/email-change/email-change.dto.js.map +1 -0
- package/dist/features/email-change/email-change.service.d.ts +74 -0
- package/dist/features/email-change/email-change.service.js +179 -0
- package/dist/features/email-change/email-change.service.js.map +1 -0
- package/dist/features/email-links/email-verification.service.d.ts +46 -0
- package/dist/features/email-links/email-verification.service.js +94 -0
- package/dist/features/email-links/email-verification.service.js.map +1 -0
- package/dist/features/email-links/link-tokens.service.d.ts +44 -0
- package/dist/features/email-links/link-tokens.service.js +122 -0
- package/dist/features/email-links/link-tokens.service.js.map +1 -0
- package/dist/features/events/auth-events.service.d.ts +52 -0
- package/dist/features/events/auth-events.service.js +117 -0
- package/dist/features/events/auth-events.service.js.map +1 -0
- package/dist/features/maintenance/data-retention.service.d.ts +60 -0
- package/dist/features/maintenance/data-retention.service.js +182 -0
- package/dist/features/maintenance/data-retention.service.js.map +1 -0
- package/dist/features/notifications/notifications.service.d.ts +40 -0
- package/dist/features/notifications/notifications.service.js +94 -0
- package/dist/features/notifications/notifications.service.js.map +1 -0
- package/dist/features/one-time-codes/one-time-codes.service.d.ts +40 -0
- package/dist/features/one-time-codes/one-time-codes.service.js +102 -0
- package/dist/features/one-time-codes/one-time-codes.service.js.map +1 -0
- package/dist/features/organizations/organizations.controller.d.ts +220 -0
- package/dist/features/organizations/organizations.controller.js +337 -0
- package/dist/features/organizations/organizations.controller.js.map +1 -0
- package/dist/features/organizations/organizations.dto.d.ts +31 -0
- package/dist/features/organizations/organizations.dto.js +100 -0
- package/dist/features/organizations/organizations.dto.js.map +1 -0
- package/dist/features/organizations/organizations.service.d.ts +274 -0
- package/dist/features/organizations/organizations.service.js +481 -0
- package/dist/features/organizations/organizations.service.js.map +1 -0
- package/dist/features/passkeys/passkeys.controller.d.ts +92 -0
- package/dist/features/passkeys/passkeys.controller.js +183 -0
- package/dist/features/passkeys/passkeys.controller.js.map +1 -0
- package/dist/features/passkeys/passkeys.dto.d.ts +13 -0
- package/dist/features/passkeys/passkeys.dto.js +50 -0
- package/dist/features/passkeys/passkeys.dto.js.map +1 -0
- package/dist/features/passkeys/passkeys.service.d.ts +159 -0
- package/dist/features/passkeys/passkeys.service.js +350 -0
- package/dist/features/passkeys/passkeys.service.js.map +1 -0
- package/dist/features/passwordless/passwordless-auth.service.d.ts +152 -0
- package/dist/features/passwordless/passwordless-auth.service.js +304 -0
- package/dist/features/passwordless/passwordless-auth.service.js.map +1 -0
- package/dist/features/passwordless/passwordless.controller.d.ts +75 -0
- package/dist/features/passwordless/passwordless.controller.js +163 -0
- package/dist/features/passwordless/passwordless.controller.js.map +1 -0
- package/dist/features/passwordless/passwordless.dto.d.ts +29 -0
- package/dist/features/passwordless/passwordless.dto.js +101 -0
- package/dist/features/passwordless/passwordless.dto.js.map +1 -0
- package/dist/features/passwords/password-auth.service.d.ts +154 -0
- package/dist/features/passwords/password-auth.service.js +317 -0
- package/dist/features/passwords/password-auth.service.js.map +1 -0
- package/dist/features/passwords/password-policy.service.d.ts +63 -0
- package/dist/features/passwords/password-policy.service.js +175 -0
- package/dist/features/passwords/password-policy.service.js.map +1 -0
- package/dist/features/passwords/password.controller.d.ts +95 -0
- package/dist/features/passwords/password.controller.js +193 -0
- package/dist/features/passwords/password.controller.js.map +1 -0
- package/dist/features/passwords/password.dto.d.ts +36 -0
- package/dist/features/passwords/password.dto.js +140 -0
- package/dist/features/passwords/password.dto.js.map +1 -0
- package/dist/features/sessions/session.service.d.ts +132 -0
- package/dist/features/sessions/session.service.js +308 -0
- package/dist/features/sessions/session.service.js.map +1 -0
- package/dist/features/sessions/session.types.d.ts +34 -0
- package/dist/features/sessions/session.types.js +3 -0
- package/dist/features/sessions/session.types.js.map +1 -0
- package/dist/features/settings/settings.defaults.d.ts +2 -0
- package/dist/features/settings/settings.defaults.js +7 -0
- package/dist/features/settings/settings.defaults.js.map +1 -0
- package/dist/features/settings/settings.service.d.ts +44 -0
- package/dist/features/settings/settings.service.js +112 -0
- package/dist/features/settings/settings.service.js.map +1 -0
- package/dist/features/settings/settings.types.d.ts +133 -0
- package/dist/features/settings/settings.types.js +3 -0
- package/dist/features/settings/settings.types.js.map +1 -0
- package/dist/features/settings/settings.validation.d.ts +33 -0
- package/dist/features/settings/settings.validation.js +614 -0
- package/dist/features/settings/settings.validation.js.map +1 -0
- package/dist/features/sign-in/sign-in-challenges.service.d.ts +54 -0
- package/dist/features/sign-in/sign-in-challenges.service.js +104 -0
- package/dist/features/sign-in/sign-in-challenges.service.js.map +1 -0
- package/dist/features/sign-in/sign-in.service.d.ts +127 -0
- package/dist/features/sign-in/sign-in.service.js +248 -0
- package/dist/features/sign-in/sign-in.service.js.map +1 -0
- package/dist/features/sign-in/sign-in.types.d.ts +22 -0
- package/dist/features/sign-in/sign-in.types.js +3 -0
- package/dist/features/sign-in/sign-in.types.js.map +1 -0
- package/dist/features/signing-keys/jwks.controller.d.ts +17 -0
- package/dist/features/signing-keys/jwks.controller.js +51 -0
- package/dist/features/signing-keys/jwks.controller.js.map +1 -0
- package/dist/features/signing-keys/signing-keys.service.d.ts +67 -0
- package/dist/features/signing-keys/signing-keys.service.js +142 -0
- package/dist/features/signing-keys/signing-keys.service.js.map +1 -0
- package/dist/features/social/social-auth.service.d.ts +100 -0
- package/dist/features/social/social-auth.service.js +232 -0
- package/dist/features/social/social-auth.service.js.map +1 -0
- package/dist/features/social/social-providers.service.d.ts +97 -0
- package/dist/features/social/social-providers.service.js +214 -0
- package/dist/features/social/social-providers.service.js.map +1 -0
- package/dist/features/social/social.controller.d.ts +54 -0
- package/dist/features/social/social.controller.js +124 -0
- package/dist/features/social/social.controller.js.map +1 -0
- package/dist/features/social/social.dto.d.ts +11 -0
- package/dist/features/social/social.dto.js +38 -0
- package/dist/features/social/social.dto.js.map +1 -0
- package/dist/features/two-factor/two-factor.controller.d.ts +84 -0
- package/dist/features/two-factor/two-factor.controller.js +174 -0
- package/dist/features/two-factor/two-factor.controller.js.map +1 -0
- package/dist/features/two-factor/two-factor.dto.d.ts +10 -0
- package/dist/features/two-factor/two-factor.dto.js +37 -0
- package/dist/features/two-factor/two-factor.dto.js.map +1 -0
- package/dist/features/two-factor/two-factor.service.d.ts +171 -0
- package/dist/features/two-factor/two-factor.service.js +348 -0
- package/dist/features/two-factor/two-factor.service.js.map +1 -0
- package/dist/features/users/sign-in-methods.service.d.ts +40 -0
- package/dist/features/users/sign-in-methods.service.js +81 -0
- package/dist/features/users/sign-in-methods.service.js.map +1 -0
- package/dist/features/users/sign-up-rules.d.ts +12 -0
- package/dist/features/users/sign-up-rules.js +29 -0
- package/dist/features/users/sign-up-rules.js.map +1 -0
- package/dist/features/users/users.service.d.ts +246 -0
- package/dist/features/users/users.service.js +509 -0
- package/dist/features/users/users.service.js.map +1 -0
- package/dist/features/users/users.types.d.ts +24 -0
- package/dist/features/users/users.types.js +3 -0
- package/dist/features/users/users.types.js.map +1 -0
- package/dist/features/webhooks/webhook-delivery.worker.d.ts +55 -0
- package/dist/features/webhooks/webhook-delivery.worker.js +175 -0
- package/dist/features/webhooks/webhook-delivery.worker.js.map +1 -0
- package/dist/features/webhooks/webhook-signature.d.ts +28 -0
- package/dist/features/webhooks/webhook-signature.js +85 -0
- package/dist/features/webhooks/webhook-signature.js.map +1 -0
- package/dist/features/webhooks/webhooks.service.d.ts +144 -0
- package/dist/features/webhooks/webhooks.service.js +238 -0
- package/dist/features/webhooks/webhooks.service.js.map +1 -0
- package/dist/guards/auth.decorators.d.ts +65 -0
- package/dist/guards/auth.decorators.js +68 -0
- package/dist/guards/auth.decorators.js.map +1 -0
- package/dist/guards/auth.guard.d.ts +47 -0
- package/dist/guards/auth.guard.js +132 -0
- package/dist/guards/auth.guard.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/console-email-sender.d.ts +15 -0
- package/dist/integrations/console-email-sender.js +26 -0
- package/dist/integrations/console-email-sender.js.map +1 -0
- package/dist/integrations/msg91-sms-sender.d.ts +28 -0
- package/dist/integrations/msg91-sms-sender.js +46 -0
- package/dist/integrations/msg91-sms-sender.js.map +1 -0
- package/docs/README.md +39 -0
- package/docs/admin-api-keys-webhooks.md +147 -0
- package/docs/api-reference.md +175 -0
- package/docs/architecture.md +119 -0
- package/docs/client-integration.md +192 -0
- package/docs/code-walkthrough.md +273 -0
- package/docs/configuration.md +229 -0
- package/docs/endpoints/README.md +151 -0
- package/docs/endpoints/account.md +363 -0
- package/docs/endpoints/admin.md +422 -0
- package/docs/endpoints/email-password.md +261 -0
- package/docs/endpoints/organizations.md +192 -0
- package/docs/endpoints/passkeys.md +195 -0
- package/docs/endpoints/passwordless.md +214 -0
- package/docs/endpoints/social.md +116 -0
- package/docs/endpoints/two-factor.md +146 -0
- package/docs/error-codes.md +100 -0
- package/docs/events.md +83 -0
- package/docs/getting-started.md +139 -0
- package/docs/manual-testing.md +215 -0
- package/docs/organizations.md +86 -0
- package/docs/production-checklist.md +60 -0
- package/docs/protecting-routes.md +147 -0
- package/docs/security.md +61 -0
- package/docs/sign-in-flows.md +294 -0
- package/package.json +92 -4
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { KeyObject } from 'node:crypto';
|
|
2
|
+
import { type JwtAlgorithm } from '../../constants.js';
|
|
3
|
+
/**
|
|
4
|
+
* A small, strict JWT implementation using only Node's built-in crypto.
|
|
5
|
+
*
|
|
6
|
+
* - We SIGN only with ES256.
|
|
7
|
+
* - We VERIFY ES256 (our own tokens) and RS256 (Google and Apple ID tokens), and only the
|
|
8
|
+
* algorithm the caller allows. This blocks the classic JWT attacks: "alg: none" and
|
|
9
|
+
* tricking the server into using the wrong kind of key.
|
|
10
|
+
*/
|
|
11
|
+
/** A JSON Web Key as it appears in a JWKS document. */
|
|
12
|
+
export interface JsonWebKey {
|
|
13
|
+
kty?: string;
|
|
14
|
+
kid?: string;
|
|
15
|
+
alg?: string;
|
|
16
|
+
use?: string;
|
|
17
|
+
crv?: string;
|
|
18
|
+
x?: string;
|
|
19
|
+
y?: string;
|
|
20
|
+
d?: string;
|
|
21
|
+
n?: string;
|
|
22
|
+
e?: string;
|
|
23
|
+
}
|
|
24
|
+
export type { JwtAlgorithm };
|
|
25
|
+
/**
|
|
26
|
+
* Turns a private key stored as a JSON Web Key into a Node KeyObject that can sign tokens.
|
|
27
|
+
*
|
|
28
|
+
* @param jsonWebKey - The full key, including the private "d" part.
|
|
29
|
+
* @returns A private KeyObject for `signJwt`.
|
|
30
|
+
* @throws If the JWK is malformed or is not a private key.
|
|
31
|
+
*/
|
|
32
|
+
export declare function privateKeyFromJwk(jsonWebKey: JsonWebKey): KeyObject;
|
|
33
|
+
/**
|
|
34
|
+
* Turns a JSON Web Key into a Node public KeyObject for checking signatures.
|
|
35
|
+
*
|
|
36
|
+
* Only the public fields are copied (n/e for RSA, crv/x/y for EC), so passing a private JWK
|
|
37
|
+
* by mistake never leaks the private part into the result.
|
|
38
|
+
*
|
|
39
|
+
* @param jsonWebKey - An RSA key (Google, Apple) or an EC key (ours).
|
|
40
|
+
* @returns A public KeyObject for `verifyJwt`.
|
|
41
|
+
* @throws If the JWK is malformed.
|
|
42
|
+
*/
|
|
43
|
+
export declare function publicKeyFromJwk(jsonWebKey: JsonWebKey): KeyObject;
|
|
44
|
+
/**
|
|
45
|
+
* Creates a signed ES256 token.
|
|
46
|
+
*
|
|
47
|
+
* @param payload - The claims to put in the token (sub, exp, iss...).
|
|
48
|
+
* @param signingKey - Our current private key and its id; the id goes into the header as "kid".
|
|
49
|
+
* @param tokenType - Goes into the header as "typ", e.g. "at+jwt" for access tokens.
|
|
50
|
+
* @returns The token as "header.payload.signature".
|
|
51
|
+
* @example
|
|
52
|
+
* const token = signJwt({ sub: userId, exp }, { keyId, privateKey }, 'at+jwt');
|
|
53
|
+
*/
|
|
54
|
+
export declare function signJwt(payload: object, signingKey: {
|
|
55
|
+
keyId: string;
|
|
56
|
+
privateKey: KeyObject;
|
|
57
|
+
}, tokenType: string): string;
|
|
58
|
+
export interface JwtVerificationRules {
|
|
59
|
+
allowedAlgorithm: JwtAlgorithm;
|
|
60
|
+
/** Accepted "iss" values. */
|
|
61
|
+
expectedIssuers: string[];
|
|
62
|
+
/** Accepted "aud" values; the token must contain at least one. */
|
|
63
|
+
expectedAudiences: string[];
|
|
64
|
+
/** Required "typ" header, or undefined to skip the check (provider tokens don't all set it). */
|
|
65
|
+
expectedTokenType?: string;
|
|
66
|
+
/** Current time in seconds; only set in tests. */
|
|
67
|
+
currentTimeSeconds?: number;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Checks a token and returns its payload if every check passes, otherwise null.
|
|
71
|
+
*
|
|
72
|
+
* Checks, in order: the shape, the algorithm, the "typ" header, the signature, the expiry
|
|
73
|
+
* ("exp", with a little clock-skew tolerance), "iat" not in the future, the issuer and the audience.
|
|
74
|
+
* Never throws for bad input, so callers can't accidentally leak a stack trace.
|
|
75
|
+
*
|
|
76
|
+
* @param token - The raw token string from the client.
|
|
77
|
+
* @param findPublicKeyById - Looks up the public key named by the token's "kid" header.
|
|
78
|
+
* @param rules - What the token must look like (algorithm, issuers, audiences, type).
|
|
79
|
+
* @returns The payload when the token is valid, or null for anything wrong with it.
|
|
80
|
+
*/
|
|
81
|
+
export declare function verifyJwt(token: string, findPublicKeyById: (keyId: string) => KeyObject | undefined, rules: JwtVerificationRules): Record<string, any> | null;
|
|
82
|
+
/**
|
|
83
|
+
* Reads the "kid" (key id) from a token's header WITHOUT checking anything, so the right public key
|
|
84
|
+
* can be fetched before calling verifyJwt.
|
|
85
|
+
*
|
|
86
|
+
* @param token - Any string that may be a JWT.
|
|
87
|
+
* @returns The key id, or undefined for anything that isn't a JWT or has no "kid".
|
|
88
|
+
*/
|
|
89
|
+
export declare function readJwtKeyId(token: string): string | undefined;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.privateKeyFromJwk = privateKeyFromJwk;
|
|
4
|
+
exports.publicKeyFromJwk = publicKeyFromJwk;
|
|
5
|
+
exports.signJwt = signJwt;
|
|
6
|
+
exports.verifyJwt = verifyJwt;
|
|
7
|
+
exports.readJwtKeyId = readJwtKeyId;
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const constants_1 = require("../../constants.js");
|
|
10
|
+
const time_1 = require("../time.js");
|
|
11
|
+
/**
|
|
12
|
+
* Turns a private key stored as a JSON Web Key into a Node KeyObject that can sign tokens.
|
|
13
|
+
*
|
|
14
|
+
* @param jsonWebKey - The full key, including the private "d" part.
|
|
15
|
+
* @returns A private KeyObject for `signJwt`.
|
|
16
|
+
* @throws If the JWK is malformed or is not a private key.
|
|
17
|
+
*/
|
|
18
|
+
function privateKeyFromJwk(jsonWebKey) {
|
|
19
|
+
return (0, node_crypto_1.createPrivateKey)({ key: jsonWebKey, format: constants_1.JWK_EXPORT_FORMAT });
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Turns a JSON Web Key into a Node public KeyObject for checking signatures.
|
|
23
|
+
*
|
|
24
|
+
* Only the public fields are copied (n/e for RSA, crv/x/y for EC), so passing a private JWK
|
|
25
|
+
* by mistake never leaks the private part into the result.
|
|
26
|
+
*
|
|
27
|
+
* @param jsonWebKey - An RSA key (Google, Apple) or an EC key (ours).
|
|
28
|
+
* @returns A public KeyObject for `verifyJwt`.
|
|
29
|
+
* @throws If the JWK is malformed.
|
|
30
|
+
*/
|
|
31
|
+
function publicKeyFromJwk(jsonWebKey) {
|
|
32
|
+
const publicPart = jsonWebKey.kty === constants_1.JWK_KEY_TYPE.rsa
|
|
33
|
+
? { kty: constants_1.JWK_KEY_TYPE.rsa, n: jsonWebKey.n, e: jsonWebKey.e }
|
|
34
|
+
: { kty: constants_1.JWK_KEY_TYPE.ellipticCurve, crv: jsonWebKey.crv, x: jsonWebKey.x, y: jsonWebKey.y };
|
|
35
|
+
return (0, node_crypto_1.createPublicKey)({ key: publicPart, format: constants_1.JWK_EXPORT_FORMAT });
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Creates a signed ES256 token.
|
|
39
|
+
*
|
|
40
|
+
* @param payload - The claims to put in the token (sub, exp, iss...).
|
|
41
|
+
* @param signingKey - Our current private key and its id; the id goes into the header as "kid".
|
|
42
|
+
* @param tokenType - Goes into the header as "typ", e.g. "at+jwt" for access tokens.
|
|
43
|
+
* @returns The token as "header.payload.signature".
|
|
44
|
+
* @example
|
|
45
|
+
* const token = signJwt({ sub: userId, exp }, { keyId, privateKey }, 'at+jwt');
|
|
46
|
+
*/
|
|
47
|
+
function signJwt(payload, signingKey, tokenType) {
|
|
48
|
+
const header = { alg: constants_1.SIGNING_ALGORITHM, typ: tokenType, kid: signingKey.keyId };
|
|
49
|
+
const headerAndPayload = [encodeJsonAsBase64Url(header), encodeJsonAsBase64Url(payload)].join(constants_1.JWT_PART_SEPARATOR);
|
|
50
|
+
const signature = (0, node_crypto_1.sign)(constants_1.HASH_ALGORITHM.sha256, Buffer.from(headerAndPayload), {
|
|
51
|
+
key: signingKey.privateKey,
|
|
52
|
+
dsaEncoding: constants_1.ECDSA_SIGNATURE_ENCODING, // JWT uses the raw r||s signature form, not DER
|
|
53
|
+
});
|
|
54
|
+
return [headerAndPayload, signature.toString(constants_1.TEXT_ENCODING.base64Url)].join(constants_1.JWT_PART_SEPARATOR);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Checks a token and returns its payload if every check passes, otherwise null.
|
|
58
|
+
*
|
|
59
|
+
* Checks, in order: the shape, the algorithm, the "typ" header, the signature, the expiry
|
|
60
|
+
* ("exp", with a little clock-skew tolerance), "iat" not in the future, the issuer and the audience.
|
|
61
|
+
* Never throws for bad input, so callers can't accidentally leak a stack trace.
|
|
62
|
+
*
|
|
63
|
+
* @param token - The raw token string from the client.
|
|
64
|
+
* @param findPublicKeyById - Looks up the public key named by the token's "kid" header.
|
|
65
|
+
* @param rules - What the token must look like (algorithm, issuers, audiences, type).
|
|
66
|
+
* @returns The payload when the token is valid, or null for anything wrong with it.
|
|
67
|
+
*/
|
|
68
|
+
function verifyJwt(token, findPublicKeyById, rules) {
|
|
69
|
+
try {
|
|
70
|
+
const parts = token.split(constants_1.JWT_PART_SEPARATOR);
|
|
71
|
+
if (parts.length !== constants_1.JWT_PART_COUNT)
|
|
72
|
+
return null;
|
|
73
|
+
const [encodedHeader, encodedPayload, encodedSignature] = parts;
|
|
74
|
+
const header = decodeBase64UrlJson(encodedHeader);
|
|
75
|
+
const payload = decodeBase64UrlJson(encodedPayload);
|
|
76
|
+
if (!header || !payload)
|
|
77
|
+
return null;
|
|
78
|
+
if (header.alg !== rules.allowedAlgorithm)
|
|
79
|
+
return null;
|
|
80
|
+
if (rules.expectedTokenType !== undefined && header.typ !== rules.expectedTokenType)
|
|
81
|
+
return null;
|
|
82
|
+
const publicKey = typeof header.kid === 'string' ? findPublicKeyById(header.kid) : undefined;
|
|
83
|
+
if (!publicKey)
|
|
84
|
+
return null;
|
|
85
|
+
const signedContent = Buffer.from([encodedHeader, encodedPayload].join(constants_1.JWT_PART_SEPARATOR));
|
|
86
|
+
const signature = Buffer.from(encodedSignature, constants_1.TEXT_ENCODING.base64Url);
|
|
87
|
+
const signatureIsValid = rules.allowedAlgorithm === constants_1.JWT_ALGORITHM.es256
|
|
88
|
+
? (0, node_crypto_1.verify)(constants_1.HASH_ALGORITHM.sha256, signedContent, { key: publicKey, dsaEncoding: constants_1.ECDSA_SIGNATURE_ENCODING }, signature)
|
|
89
|
+
: (0, node_crypto_1.verify)(constants_1.HASH_ALGORITHM.sha256, signedContent, publicKey, signature);
|
|
90
|
+
if (!signatureIsValid)
|
|
91
|
+
return null;
|
|
92
|
+
const now = rules.currentTimeSeconds ?? (0, time_1.currentUnixTimeSeconds)();
|
|
93
|
+
const hasExpired = typeof payload.exp !== 'number' || payload.exp + constants_1.CLOCK_SKEW_TOLERANCE_SECONDS <= now;
|
|
94
|
+
const issuedInFuture = typeof payload.iat === 'number' && payload.iat - constants_1.CLOCK_SKEW_TOLERANCE_SECONDS > now;
|
|
95
|
+
if (hasExpired || issuedInFuture)
|
|
96
|
+
return null;
|
|
97
|
+
if (!rules.expectedIssuers.includes(payload.iss))
|
|
98
|
+
return null;
|
|
99
|
+
const tokenAudiences = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
|
|
100
|
+
if (!tokenAudiences.some((audience) => rules.expectedAudiences.includes(audience)))
|
|
101
|
+
return null;
|
|
102
|
+
return payload;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Reads the "kid" (key id) from a token's header WITHOUT checking anything, so the right public key
|
|
110
|
+
* can be fetched before calling verifyJwt.
|
|
111
|
+
*
|
|
112
|
+
* @param token - Any string that may be a JWT.
|
|
113
|
+
* @returns The key id, or undefined for anything that isn't a JWT or has no "kid".
|
|
114
|
+
*/
|
|
115
|
+
function readJwtKeyId(token) {
|
|
116
|
+
try {
|
|
117
|
+
const header = decodeBase64UrlJson(token.split(constants_1.JWT_PART_SEPARATOR)[0]);
|
|
118
|
+
return typeof header?.kid === 'string' ? header.kid : undefined;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Encodes a value as JSON, then base64url (the form used in each part of a JWT).
|
|
126
|
+
*
|
|
127
|
+
* @param value - The header or payload object.
|
|
128
|
+
* @returns The base64url text.
|
|
129
|
+
*/
|
|
130
|
+
function encodeJsonAsBase64Url(value) {
|
|
131
|
+
return Buffer.from(JSON.stringify(value)).toString(constants_1.TEXT_ENCODING.base64Url);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Reverses `encodeJsonAsBase64Url`.
|
|
135
|
+
*
|
|
136
|
+
* @param encoded - One base64url part of a JWT.
|
|
137
|
+
* @returns The decoded object, or null when the JSON is not a plain object.
|
|
138
|
+
* @throws If the text is not valid JSON (callers catch this).
|
|
139
|
+
*/
|
|
140
|
+
function decodeBase64UrlJson(encoded) {
|
|
141
|
+
const decoded = JSON.parse(Buffer.from(encoded, constants_1.TEXT_ENCODING.base64Url).toString(constants_1.TEXT_ENCODING.utf8));
|
|
142
|
+
return decoded && typeof decoded === 'object' && !Array.isArray(decoded) ? decoded : null;
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=jwt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.js","sourceRoot":"","sources":["../../../src/common/crypto/jwt.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { TokenPrefix } from '../../constants.js';
|
|
2
|
+
/**
|
|
3
|
+
* Random secrets we give out, and how we store them.
|
|
4
|
+
*
|
|
5
|
+
* Rule: the plain secret is shown to the user exactly once. The database only ever holds a hash,
|
|
6
|
+
* so even a full database leak gives an attacker nothing they can use.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Creates a long random token like "rt_Xy3...".
|
|
10
|
+
*
|
|
11
|
+
* @param prefix - Says what the token is for (e.g. "rt" for refresh tokens), which helps when one leaks.
|
|
12
|
+
* @returns `plainToken` to hand to the user once, and `tokenHash` to store in the database.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createRandomToken(prefix: TokenPrefix): {
|
|
15
|
+
plainToken: string;
|
|
16
|
+
tokenHash: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Checks that a value has exactly the shape of a token we created with this prefix.
|
|
20
|
+
* A cheap check to run before any database lookup.
|
|
21
|
+
*
|
|
22
|
+
* @param value - Anything the client sent.
|
|
23
|
+
* @param prefix - The prefix the token should start with.
|
|
24
|
+
* @returns True when the value looks like one of our tokens.
|
|
25
|
+
*/
|
|
26
|
+
export declare function hasRandomTokenFormat(value: unknown, prefix: TokenPrefix): value is string;
|
|
27
|
+
/**
|
|
28
|
+
* Hashes text with SHA-256. Used for long random tokens, which are too random to guess,
|
|
29
|
+
* so no salt or slow hash is needed.
|
|
30
|
+
*
|
|
31
|
+
* @param value - The text to hash.
|
|
32
|
+
* @returns The hash as 64 hex characters.
|
|
33
|
+
*/
|
|
34
|
+
export declare function sha256Hex(value: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Creates a numeric code such as "482915" for email or SMS sign-in.
|
|
37
|
+
* Uses crypto.randomInt, which has no bias towards particular digits.
|
|
38
|
+
*
|
|
39
|
+
* @param numberOfDigits - How long the code should be.
|
|
40
|
+
* @returns The code as a string (it may start with 0).
|
|
41
|
+
*/
|
|
42
|
+
export declare function createNumericCode(numberOfDigits: number): string;
|
|
43
|
+
/**
|
|
44
|
+
* Hashes a short code (SMS/email codes, backup codes). A random salt per code means two users
|
|
45
|
+
* with the same code get different hashes. Short codes are also protected by expiry and attempt limits.
|
|
46
|
+
*
|
|
47
|
+
* @param code - The plain code.
|
|
48
|
+
* @param salt - The code's own salt, from `createSalt`.
|
|
49
|
+
* @returns The hash to store or compare.
|
|
50
|
+
*/
|
|
51
|
+
export declare function hashShortCode(code: string, salt: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Creates a random salt for `hashShortCode`.
|
|
54
|
+
*
|
|
55
|
+
* @returns The salt as hex text.
|
|
56
|
+
*/
|
|
57
|
+
export declare function createSalt(): string;
|
|
58
|
+
/**
|
|
59
|
+
* Creates a backup code like "k7m2x9p4" (shown to users as "k7m2-x9p4").
|
|
60
|
+
*
|
|
61
|
+
* @returns The plain backup code.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createBackupCode(): string;
|
|
64
|
+
/**
|
|
65
|
+
* Removes dashes, spaces and capitals, so "K7M2-X9P4" matches "k7m2x9p4".
|
|
66
|
+
*
|
|
67
|
+
* @param typedCode - The backup code as the user typed it.
|
|
68
|
+
* @returns The code in its stored form.
|
|
69
|
+
*/
|
|
70
|
+
export declare function normalizeBackupCode(typedCode: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* Compares two strings in constant time, so the time taken does not hint at how many characters matched.
|
|
73
|
+
*
|
|
74
|
+
* @param first - One string.
|
|
75
|
+
* @param second - The other string.
|
|
76
|
+
* @returns True when both strings are identical.
|
|
77
|
+
*/
|
|
78
|
+
export declare function constantTimeEquals(first: string, second: string): boolean;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRandomToken = createRandomToken;
|
|
4
|
+
exports.hasRandomTokenFormat = hasRandomTokenFormat;
|
|
5
|
+
exports.sha256Hex = sha256Hex;
|
|
6
|
+
exports.createNumericCode = createNumericCode;
|
|
7
|
+
exports.hashShortCode = hashShortCode;
|
|
8
|
+
exports.createSalt = createSalt;
|
|
9
|
+
exports.createBackupCode = createBackupCode;
|
|
10
|
+
exports.normalizeBackupCode = normalizeBackupCode;
|
|
11
|
+
exports.constantTimeEquals = constantTimeEquals;
|
|
12
|
+
const node_crypto_1 = require("node:crypto");
|
|
13
|
+
const constants_1 = require("../../constants.js");
|
|
14
|
+
/**
|
|
15
|
+
* Random secrets we give out, and how we store them.
|
|
16
|
+
*
|
|
17
|
+
* Rule: the plain secret is shown to the user exactly once. The database only ever holds a hash,
|
|
18
|
+
* so even a full database leak gives an attacker nothing they can use.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Creates a long random token like "rt_Xy3...".
|
|
22
|
+
*
|
|
23
|
+
* @param prefix - Says what the token is for (e.g. "rt" for refresh tokens), which helps when one leaks.
|
|
24
|
+
* @returns `plainToken` to hand to the user once, and `tokenHash` to store in the database.
|
|
25
|
+
*/
|
|
26
|
+
function createRandomToken(prefix) {
|
|
27
|
+
const plainToken = `${prefix}${constants_1.TOKEN_PREFIX_SEPARATOR}${(0, node_crypto_1.randomBytes)(constants_1.RANDOM_TOKEN_BYTES).toString(constants_1.TEXT_ENCODING.base64Url)}`;
|
|
28
|
+
return { plainToken, tokenHash: sha256Hex(plainToken) };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Checks that a value has exactly the shape of a token we created with this prefix.
|
|
32
|
+
* A cheap check to run before any database lookup.
|
|
33
|
+
*
|
|
34
|
+
* @param value - Anything the client sent.
|
|
35
|
+
* @param prefix - The prefix the token should start with.
|
|
36
|
+
* @returns True when the value looks like one of our tokens.
|
|
37
|
+
*/
|
|
38
|
+
function hasRandomTokenFormat(value, prefix) {
|
|
39
|
+
const base64UrlLengthOf32Bytes = constants_1.RANDOM_TOKEN_BASE64URL_LENGTH;
|
|
40
|
+
return (typeof value === 'string' &&
|
|
41
|
+
value.length === prefix.length + 1 + base64UrlLengthOf32Bytes &&
|
|
42
|
+
value.startsWith(`${prefix}${constants_1.TOKEN_PREFIX_SEPARATOR}`) &&
|
|
43
|
+
constants_1.BASE64URL_TEXT_PATTERN.test(value));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Hashes text with SHA-256. Used for long random tokens, which are too random to guess,
|
|
47
|
+
* so no salt or slow hash is needed.
|
|
48
|
+
*
|
|
49
|
+
* @param value - The text to hash.
|
|
50
|
+
* @returns The hash as 64 hex characters.
|
|
51
|
+
*/
|
|
52
|
+
function sha256Hex(value) {
|
|
53
|
+
return (0, node_crypto_1.createHash)(constants_1.HASH_ALGORITHM.sha256).update(value, constants_1.TEXT_ENCODING.utf8).digest(constants_1.TEXT_ENCODING.hex);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Creates a numeric code such as "482915" for email or SMS sign-in.
|
|
57
|
+
* Uses crypto.randomInt, which has no bias towards particular digits.
|
|
58
|
+
*
|
|
59
|
+
* @param numberOfDigits - How long the code should be.
|
|
60
|
+
* @returns The code as a string (it may start with 0).
|
|
61
|
+
*/
|
|
62
|
+
function createNumericCode(numberOfDigits) {
|
|
63
|
+
return Array.from({ length: numberOfDigits }, () => (0, node_crypto_1.randomInt)(0, constants_1.DECIMAL_BASE)).join('');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Hashes a short code (SMS/email codes, backup codes). A random salt per code means two users
|
|
67
|
+
* with the same code get different hashes. Short codes are also protected by expiry and attempt limits.
|
|
68
|
+
*
|
|
69
|
+
* @param code - The plain code.
|
|
70
|
+
* @param salt - The code's own salt, from `createSalt`.
|
|
71
|
+
* @returns The hash to store or compare.
|
|
72
|
+
*/
|
|
73
|
+
function hashShortCode(code, salt) {
|
|
74
|
+
return sha256Hex(`${salt}${constants_1.SALTED_CODE_SEPARATOR}${code}`);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Creates a random salt for `hashShortCode`.
|
|
78
|
+
*
|
|
79
|
+
* @returns The salt as hex text.
|
|
80
|
+
*/
|
|
81
|
+
function createSalt() {
|
|
82
|
+
return (0, node_crypto_1.randomBytes)(constants_1.SHORT_CODE_SALT_BYTES).toString(constants_1.TEXT_ENCODING.hex);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Creates a backup code like "k7m2x9p4" (shown to users as "k7m2-x9p4").
|
|
86
|
+
*
|
|
87
|
+
* @returns The plain backup code.
|
|
88
|
+
*/
|
|
89
|
+
function createBackupCode() {
|
|
90
|
+
return Array.from({ length: constants_1.BACKUP_CODE_LENGTH }, () => constants_1.BACKUP_CODE_ALPHABET[(0, node_crypto_1.randomInt)(0, constants_1.BACKUP_CODE_ALPHABET.length)]).join('');
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Removes dashes, spaces and capitals, so "K7M2-X9P4" matches "k7m2x9p4".
|
|
94
|
+
*
|
|
95
|
+
* @param typedCode - The backup code as the user typed it.
|
|
96
|
+
* @returns The code in its stored form.
|
|
97
|
+
*/
|
|
98
|
+
function normalizeBackupCode(typedCode) {
|
|
99
|
+
return typedCode.toLowerCase().replace(constants_1.BACKUP_CODE_FORMATTING_CHARACTERS_PATTERN, '');
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Compares two strings in constant time, so the time taken does not hint at how many characters matched.
|
|
103
|
+
*
|
|
104
|
+
* @param first - One string.
|
|
105
|
+
* @param second - The other string.
|
|
106
|
+
* @returns True when both strings are identical.
|
|
107
|
+
*/
|
|
108
|
+
function constantTimeEquals(first, second) {
|
|
109
|
+
const firstBytes = Buffer.from(first);
|
|
110
|
+
const secondBytes = Buffer.from(second);
|
|
111
|
+
return firstBytes.length === secondBytes.length && (0, node_crypto_1.timingSafeEqual)(firstBytes, secondBytes);
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=random-secrets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"random-secrets.js","sourceRoot":"","sources":["../../../src/common/crypto/random-secrets.ts"],"names":[],"mappings":";;AA8BA,8CAGC;AAUD,oDAQC;AASD,8BAEC;AASD,8CAEC;AAUD,sCAEC;AAOD,gCAEC;AAOD,4CAEC;AAQD,kDAEC;AASD,gDAIC;AA9HD,6CAAkF;AAClF,2CAcqB;AAErB;;;;;GAKG;AAEH;;;;;GAKG;AACH,SAAgB,iBAAiB,CAAC,MAAmB;IACnD,MAAM,UAAU,GAAG,GAAG,MAAM,GAAG,kCAAsB,GAAG,IAAA,yBAAW,EAAC,8BAAkB,CAAC,CAAC,QAAQ,CAAC,yBAAa,CAAC,SAAS,CAAC,EAAE,CAAC;IAC5H,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,CAAC,UAAU,CAAC,EAAE,CAAC;AAC1D,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,oBAAoB,CAAC,KAAc,EAAE,MAAmB;IACtE,MAAM,wBAAwB,GAAG,yCAA6B,CAAC;IAC/D,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,GAAG,CAAC,GAAG,wBAAwB;QAC7D,KAAK,CAAC,UAAU,CAAC,GAAG,MAAM,GAAG,kCAAsB,EAAE,CAAC;QACtD,kCAAsB,CAAC,IAAI,CAAC,KAAK,CAAC,CACnC,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,SAAS,CAAC,KAAa;IACrC,OAAO,IAAA,wBAAU,EAAC,0BAAc,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,yBAAa,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,yBAAa,CAAC,GAAG,CAAC,CAAC;AACvG,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,iBAAiB,CAAC,cAAsB;IACtD,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,EAAE,GAAG,EAAE,CAAC,IAAA,uBAAS,EAAC,CAAC,EAAE,wBAAY,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC3F,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,aAAa,CAAC,IAAY,EAAE,IAAY;IACtD,OAAO,SAAS,CAAC,GAAG,IAAI,GAAG,iCAAqB,GAAG,IAAI,EAAE,CAAC,CAAC;AAC7D,CAAC;AAED;;;;GAIG;AACH,SAAgB,UAAU;IACxB,OAAO,IAAA,yBAAW,EAAC,iCAAqB,CAAC,CAAC,QAAQ,CAAC,yBAAa,CAAC,GAAG,CAAC,CAAC;AACxE,CAAC;AAED;;;;GAIG;AACH,SAAgB,gBAAgB;IAC9B,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,8BAAkB,EAAE,EAAE,GAAG,EAAE,CAAC,gCAAoB,CAAC,IAAA,uBAAS,EAAC,CAAC,EAAE,gCAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACpI,CAAC;AAED;;;;;GAKG;AACH,SAAgB,mBAAmB,CAAC,SAAiB;IACnD,OAAO,SAAS,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,qDAAyC,EAAE,EAAE,CAAC,CAAC;AACxF,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,kBAAkB,CAAC,KAAa,EAAE,MAAc;IAC9D,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtC,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACxC,OAAO,UAAU,CAAC,MAAM,KAAK,WAAW,CAAC,MAAM,IAAI,IAAA,6BAAe,EAAC,UAAU,EAAE,WAAW,CAAC,CAAC;AAC9F,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { KeyObject } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Downloads and caches a provider's public signing keys (a JWKS URL such as Google's).
|
|
4
|
+
*
|
|
5
|
+
* - Keys are cached for hours, because providers change them rarely.
|
|
6
|
+
* - If a token names a key we don't have, the keys are re-downloaded (providers rotate keys),
|
|
7
|
+
* but at most once per UNKNOWN_KEY_REFETCH_WAIT_SECONDS so junk tokens can't make us spam the provider.
|
|
8
|
+
*/
|
|
9
|
+
export declare class RemotePublicKeys {
|
|
10
|
+
private readonly keysUrl;
|
|
11
|
+
private keysById;
|
|
12
|
+
private loadedAt;
|
|
13
|
+
private lastForcedReloadAt;
|
|
14
|
+
private loading?;
|
|
15
|
+
constructor(keysUrl: string);
|
|
16
|
+
/**
|
|
17
|
+
* Finds a provider public key by id, downloading the key list first if needed.
|
|
18
|
+
* Never throws: a download failure just means "unknown key".
|
|
19
|
+
*
|
|
20
|
+
* @param keyId - The "kid" from the token header.
|
|
21
|
+
* @returns The public key, or undefined if the provider doesn't have one with this id.
|
|
22
|
+
*/
|
|
23
|
+
findKey(keyId: string): Promise<KeyObject | undefined>;
|
|
24
|
+
/**
|
|
25
|
+
* Makes sure the key named by `keyId` has been downloaded (if it exists), so it can then be read
|
|
26
|
+
* synchronously with `findLoadedKey`, e.g. from inside `verifyJwt`'s key lookup.
|
|
27
|
+
*
|
|
28
|
+
* @param keyId - The "kid" from the token header, or undefined (then nothing happens).
|
|
29
|
+
*/
|
|
30
|
+
ensureLoaded(keyId: string | undefined): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Reads a key from the cache without downloading anything. Call `ensureLoaded` first.
|
|
33
|
+
*
|
|
34
|
+
* @param keyId - The "kid" from the token header.
|
|
35
|
+
* @returns The cached public key, or undefined.
|
|
36
|
+
*/
|
|
37
|
+
findLoadedKey(keyId: string): KeyObject | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Downloads the keys again. If a download is already running, waits for that one instead.
|
|
40
|
+
*
|
|
41
|
+
* @returns Resolves when the download has finished (successfully or not).
|
|
42
|
+
*/
|
|
43
|
+
private reload;
|
|
44
|
+
/**
|
|
45
|
+
* Fetches the JWKS URL and replaces the cache with the RSA keys it contains.
|
|
46
|
+
* On any failure the old keys are kept.
|
|
47
|
+
*/
|
|
48
|
+
private download;
|
|
49
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RemotePublicKeys = void 0;
|
|
4
|
+
const constants_1 = require("../../constants.js");
|
|
5
|
+
const jwt_1 = require("./jwt");
|
|
6
|
+
/**
|
|
7
|
+
* Downloads and caches a provider's public signing keys (a JWKS URL such as Google's).
|
|
8
|
+
*
|
|
9
|
+
* - Keys are cached for hours, because providers change them rarely.
|
|
10
|
+
* - If a token names a key we don't have, the keys are re-downloaded (providers rotate keys),
|
|
11
|
+
* but at most once per UNKNOWN_KEY_REFETCH_WAIT_SECONDS so junk tokens can't make us spam the provider.
|
|
12
|
+
*/
|
|
13
|
+
class RemotePublicKeys {
|
|
14
|
+
keysUrl;
|
|
15
|
+
keysById = new Map();
|
|
16
|
+
loadedAt = 0;
|
|
17
|
+
lastForcedReloadAt = 0;
|
|
18
|
+
loading;
|
|
19
|
+
constructor(keysUrl) {
|
|
20
|
+
this.keysUrl = keysUrl;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Finds a provider public key by id, downloading the key list first if needed.
|
|
24
|
+
* Never throws: a download failure just means "unknown key".
|
|
25
|
+
*
|
|
26
|
+
* @param keyId - The "kid" from the token header.
|
|
27
|
+
* @returns The public key, or undefined if the provider doesn't have one with this id.
|
|
28
|
+
*/
|
|
29
|
+
async findKey(keyId) {
|
|
30
|
+
const cacheIsStale = Date.now() - this.loadedAt > constants_1.SOCIAL_PUBLIC_KEYS_CACHE_SECONDS * constants_1.SECOND_IN_MILLISECONDS;
|
|
31
|
+
if (cacheIsStale)
|
|
32
|
+
await this.reload();
|
|
33
|
+
if (!this.keysById.has(keyId)) {
|
|
34
|
+
const mayForceReload = Date.now() - this.lastForcedReloadAt > constants_1.UNKNOWN_KEY_REFETCH_WAIT_SECONDS * constants_1.SECOND_IN_MILLISECONDS;
|
|
35
|
+
if (mayForceReload) {
|
|
36
|
+
this.lastForcedReloadAt = Date.now();
|
|
37
|
+
await this.reload();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return this.keysById.get(keyId);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Makes sure the key named by `keyId` has been downloaded (if it exists), so it can then be read
|
|
44
|
+
* synchronously with `findLoadedKey`, e.g. from inside `verifyJwt`'s key lookup.
|
|
45
|
+
*
|
|
46
|
+
* @param keyId - The "kid" from the token header, or undefined (then nothing happens).
|
|
47
|
+
*/
|
|
48
|
+
async ensureLoaded(keyId) {
|
|
49
|
+
if (keyId)
|
|
50
|
+
await this.findKey(keyId);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Reads a key from the cache without downloading anything. Call `ensureLoaded` first.
|
|
54
|
+
*
|
|
55
|
+
* @param keyId - The "kid" from the token header.
|
|
56
|
+
* @returns The cached public key, or undefined.
|
|
57
|
+
*/
|
|
58
|
+
findLoadedKey(keyId) {
|
|
59
|
+
return this.keysById.get(keyId);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Downloads the keys again. If a download is already running, waits for that one instead.
|
|
63
|
+
*
|
|
64
|
+
* @returns Resolves when the download has finished (successfully or not).
|
|
65
|
+
*/
|
|
66
|
+
reload() {
|
|
67
|
+
// If a download is already running, wait for that one instead of starting another.
|
|
68
|
+
this.loading ??= this.download().finally(() => (this.loading = undefined));
|
|
69
|
+
return this.loading;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Fetches the JWKS URL and replaces the cache with the RSA keys it contains.
|
|
73
|
+
* On any failure the old keys are kept.
|
|
74
|
+
*/
|
|
75
|
+
async download() {
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetch(this.keysUrl, { signal: AbortSignal.timeout(constants_1.SOCIAL_PROVIDER_TIMEOUT_MS) });
|
|
78
|
+
if (!response.ok)
|
|
79
|
+
return;
|
|
80
|
+
const { keys } = (await response.json());
|
|
81
|
+
if (!Array.isArray(keys))
|
|
82
|
+
return;
|
|
83
|
+
const freshKeys = new Map();
|
|
84
|
+
for (const jsonWebKey of keys) {
|
|
85
|
+
if (!jsonWebKey.kid || jsonWebKey.kty !== constants_1.JWK_KEY_TYPE.rsa)
|
|
86
|
+
continue;
|
|
87
|
+
try {
|
|
88
|
+
freshKeys.set(jsonWebKey.kid, (0, jwt_1.publicKeyFromJwk)(jsonWebKey));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Skip a malformed key rather than losing all of them.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (freshKeys.size > 0) {
|
|
95
|
+
this.keysById = freshKeys;
|
|
96
|
+
this.loadedAt = Date.now();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Network problem: keep the keys we had. Tokens signed by unknown keys will fail.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
exports.RemotePublicKeys = RemotePublicKeys;
|
|
105
|
+
//# sourceMappingURL=remote-public-keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-public-keys.js","sourceRoot":"","sources":["../../../src/common/crypto/remote-public-keys.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time-based one-time passwords (RFC 6238): the 6-digit codes shown by Google Authenticator,
|
|
3
|
+
* Microsoft Authenticator, 1Password, Authy and others.
|
|
4
|
+
*
|
|
5
|
+
* The user's app and our server share a secret. Every 30 seconds both compute
|
|
6
|
+
* HMAC-SHA1(secret, current 30-second step) and turn it into 6 digits. If they match, the user
|
|
7
|
+
* has the phone that holds the secret.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new random secret, in the Base32 form authenticator apps expect.
|
|
11
|
+
*
|
|
12
|
+
* @returns The secret, e.g. "JBSWY3DPEHPK3PXP...".
|
|
13
|
+
*/
|
|
14
|
+
export declare function createTotpSecret(): string;
|
|
15
|
+
/**
|
|
16
|
+
* Builds the link inside the QR code the user scans:
|
|
17
|
+
* otpauth://totp/Issuer:account?secret=...&issuer=Issuer
|
|
18
|
+
*
|
|
19
|
+
* @param secret - The Base32 secret from `createTotpSecret`.
|
|
20
|
+
* @param accountName - Shown in the app under the issuer, usually the user's email.
|
|
21
|
+
* @param issuerName - Your app's name, shown in the authenticator app.
|
|
22
|
+
* @returns The otpauth:// URL to turn into a QR code.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildAuthenticatorAppUrl(secret: string, accountName: string, issuerName: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Computes the 6-digit code for one 30-second time step (RFC 6238).
|
|
27
|
+
*
|
|
28
|
+
* @param secret - The Base32 secret shared with the authenticator app.
|
|
29
|
+
* @param timeStep - Which 30-second window, from `timeStepAt`.
|
|
30
|
+
* @returns The code as a zero-padded string, e.g. "042913".
|
|
31
|
+
*/
|
|
32
|
+
export declare function totpCodeForTimeStep(secret: string, timeStep: number): string;
|
|
33
|
+
/**
|
|
34
|
+
* Converts a time into its 30-second TOTP window number.
|
|
35
|
+
*
|
|
36
|
+
* @param unixTimeSeconds - Seconds since 1970.
|
|
37
|
+
* @returns The time step that contains this moment.
|
|
38
|
+
*/
|
|
39
|
+
export declare function timeStepAt(unixTimeSeconds: number): number;
|
|
40
|
+
/**
|
|
41
|
+
* Checks a code the user typed from their authenticator app.
|
|
42
|
+
*
|
|
43
|
+
* Codes from one step before or after the current one are also accepted, because phone clocks drift.
|
|
44
|
+
* `lastAcceptedTimeStep` blocks replay: a code that was already used (or an older one) is refused,
|
|
45
|
+
* so someone who sees a code over the user's shoulder cannot use it again.
|
|
46
|
+
*
|
|
47
|
+
* @param secret - The user's Base32 secret.
|
|
48
|
+
* @param typedCode - What the user typed; spaces are ignored.
|
|
49
|
+
* @param lastAcceptedTimeStep - The step of the last code accepted for this user, or null if none.
|
|
50
|
+
* @param unixTimeSeconds - The current time; only passed in tests.
|
|
51
|
+
* @returns The matching time step (save it as the new lastAcceptedTimeStep), or null if wrong.
|
|
52
|
+
*/
|
|
53
|
+
export declare function verifyTotpCode(secret: string, typedCode: string, lastAcceptedTimeStep: number | null, unixTimeSeconds?: number): number | null;
|
|
54
|
+
/**
|
|
55
|
+
* Encodes bytes as Base32 (RFC 4648, no padding), the format authenticator apps use for secrets.
|
|
56
|
+
*
|
|
57
|
+
* @param bytes - The raw bytes.
|
|
58
|
+
* @returns The Base32 text.
|
|
59
|
+
*/
|
|
60
|
+
export declare function encodeBase32(bytes: Buffer): string;
|
|
61
|
+
/**
|
|
62
|
+
* Decodes Base32 text back into bytes. Lower case and "=" padding are accepted.
|
|
63
|
+
*
|
|
64
|
+
* @param text - The Base32 text.
|
|
65
|
+
* @returns The decoded bytes.
|
|
66
|
+
* @throws If the text contains a character that is not in the Base32 alphabet.
|
|
67
|
+
*/
|
|
68
|
+
export declare function decodeBase32(text: string): Buffer;
|