@tstdl/base 0.93.178 → 0.93.180

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/api/response.js +4 -3
  2. package/api/server/gateway.js +9 -3
  3. package/audit/auditor.d.ts +1 -2
  4. package/audit/drizzle/{0000_lumpy_thunderball.sql → 0000_shallow_elektra.sql} +1 -1
  5. package/audit/drizzle/meta/0000_snapshot.json +2 -2
  6. package/audit/drizzle/meta/_journal.json +2 -2
  7. package/authentication/README.md +87 -42
  8. package/authentication/authentication.api.d.ts +392 -53
  9. package/authentication/authentication.api.js +133 -28
  10. package/authentication/client/api.client.d.ts +3 -3
  11. package/authentication/client/api.client.js +4 -4
  12. package/authentication/client/authentication.service.d.ts +93 -23
  13. package/authentication/client/authentication.service.js +113 -28
  14. package/authentication/client/http-client.middleware.d.ts +1 -1
  15. package/authentication/client/http-client.middleware.js +5 -4
  16. package/authentication/client/module.d.ts +1 -1
  17. package/authentication/client/module.js +2 -2
  18. package/authentication/errors/index.d.ts +1 -1
  19. package/authentication/errors/index.js +1 -1
  20. package/authentication/errors/password-requirements.error.d.ts +5 -0
  21. package/authentication/errors/{secret-requirements.error.js → password-requirements.error.js} +2 -2
  22. package/authentication/models/authentication-password.model.d.ts +8 -0
  23. package/authentication/models/{authentication-credentials.model.js → authentication-password.model.js} +11 -17
  24. package/authentication/models/authentication-session.model.d.ts +0 -2
  25. package/authentication/models/authentication-session.model.js +1 -7
  26. package/authentication/models/authentication-totp-recovery-code.model.d.ts +6 -0
  27. package/authentication/models/authentication-totp-recovery-code.model.js +34 -0
  28. package/authentication/models/authentication-totp.model.d.ts +19 -0
  29. package/authentication/models/authentication-totp.model.js +51 -0
  30. package/authentication/models/authentication-used-totp-token.model.d.ts +5 -0
  31. package/authentication/models/authentication-used-totp-token.model.js +32 -0
  32. package/authentication/models/index.d.ts +6 -3
  33. package/authentication/models/index.js +6 -3
  34. package/authentication/models/{init-secret-reset-data.model.d.ts → init-password-reset-data.model.d.ts} +3 -3
  35. package/authentication/models/{init-secret-reset-data.model.js → init-password-reset-data.model.js} +5 -5
  36. package/authentication/models/password-check-result.model.d.ts +3 -0
  37. package/authentication/models/{secret-check-result.model.js → password-check-result.model.js} +6 -6
  38. package/authentication/models/subject.model.d.ts +0 -6
  39. package/authentication/models/subject.model.js +0 -6
  40. package/authentication/models/token.model.d.ts +16 -2
  41. package/authentication/server/authentication-ancillary.service.d.ts +6 -6
  42. package/authentication/server/authentication-ancillary.service.js +1 -1
  43. package/authentication/server/authentication-password-requirements.validator.d.ts +55 -0
  44. package/authentication/server/{authentication-secret-requirements.validator.js → authentication-password-requirements.validator.js} +22 -22
  45. package/authentication/server/authentication.api-controller.d.ts +55 -27
  46. package/authentication/server/authentication.api-controller.js +214 -39
  47. package/authentication/server/authentication.audit.d.ts +42 -5
  48. package/authentication/server/authentication.service.d.ts +182 -93
  49. package/authentication/server/authentication.service.js +628 -206
  50. package/authentication/server/drizzle/{0000_soft_tag.sql → 0000_odd_echo.sql} +59 -13
  51. package/authentication/server/drizzle/meta/0000_snapshot.json +345 -32
  52. package/authentication/server/drizzle/meta/_journal.json +2 -2
  53. package/authentication/server/helper.d.ts +16 -16
  54. package/authentication/server/helper.js +33 -34
  55. package/authentication/server/index.d.ts +1 -1
  56. package/authentication/server/index.js +1 -1
  57. package/authentication/server/module.d.ts +2 -2
  58. package/authentication/server/module.js +4 -2
  59. package/authentication/server/schemas.d.ts +11 -7
  60. package/authentication/server/schemas.js +7 -3
  61. package/authentication/tests/authentication-password-requirements.validator.test.js +29 -0
  62. package/authentication/tests/authentication.api-controller.test.js +49 -15
  63. package/authentication/tests/authentication.client-error-handling.test.js +3 -2
  64. package/authentication/tests/authentication.client-middleware.test.js +5 -5
  65. package/authentication/tests/authentication.client-service-methods.test.js +28 -14
  66. package/authentication/tests/authentication.client-service-refresh.test.js +7 -6
  67. package/authentication/tests/authentication.client-service.test.js +10 -8
  68. package/authentication/tests/authentication.service.test.js +37 -29
  69. package/authentication/tests/authentication.test-ancillary-service.d.ts +1 -1
  70. package/authentication/tests/authentication.test-ancillary-service.js +1 -1
  71. package/authentication/tests/brute-force-protection.test.js +211 -0
  72. package/authentication/tests/helper.test.js +25 -21
  73. package/authentication/tests/password-requirements.error.test.js +14 -0
  74. package/authentication/tests/remember.api.test.js +22 -14
  75. package/authentication/tests/remember.service.test.js +23 -16
  76. package/authentication/tests/subject.service.test.js +2 -2
  77. package/authentication/tests/suspended-subject.test.d.ts +1 -0
  78. package/authentication/tests/suspended-subject.test.js +120 -0
  79. package/authentication/tests/totp.enrollment.test.d.ts +1 -0
  80. package/authentication/tests/totp.enrollment.test.js +123 -0
  81. package/authentication/tests/totp.login.test.d.ts +1 -0
  82. package/authentication/tests/totp.login.test.js +213 -0
  83. package/authentication/tests/totp.recovery-codes.test.d.ts +1 -0
  84. package/authentication/tests/totp.recovery-codes.test.js +97 -0
  85. package/authentication/tests/totp.status.test.d.ts +1 -0
  86. package/authentication/tests/totp.status.test.js +72 -0
  87. package/circuit-breaker/postgres/drizzle/{0000_cooing_korath.sql → 0000_same_captain_cross.sql} +1 -1
  88. package/circuit-breaker/postgres/drizzle/meta/0000_snapshot.json +2 -2
  89. package/circuit-breaker/postgres/drizzle/meta/_journal.json +2 -2
  90. package/cryptography/cryptography.d.ts +336 -0
  91. package/cryptography/cryptography.js +328 -0
  92. package/cryptography/index.d.ts +4 -0
  93. package/cryptography/index.js +4 -0
  94. package/{utils → cryptography}/jwt.d.ts +22 -4
  95. package/{utils → cryptography}/jwt.js +36 -18
  96. package/cryptography/module.d.ts +35 -0
  97. package/cryptography/module.js +148 -0
  98. package/cryptography/tests/cryptography.test.d.ts +1 -0
  99. package/cryptography/tests/cryptography.test.js +175 -0
  100. package/cryptography/tests/jwt.test.d.ts +1 -0
  101. package/cryptography/tests/jwt.test.js +54 -0
  102. package/cryptography/tests/modern.test.d.ts +1 -0
  103. package/cryptography/tests/modern.test.js +105 -0
  104. package/cryptography/tests/module.test.d.ts +1 -0
  105. package/cryptography/tests/module.test.js +100 -0
  106. package/cryptography/tests/totp.test.d.ts +1 -0
  107. package/cryptography/tests/totp.test.js +108 -0
  108. package/cryptography/totp.d.ts +96 -0
  109. package/cryptography/totp.js +123 -0
  110. package/document-management/server/drizzle/{0000_curious_nighthawk.sql → 0000_sharp_scream.sql} +21 -21
  111. package/document-management/server/drizzle/meta/0000_snapshot.json +22 -22
  112. package/document-management/server/drizzle/meta/_journal.json +2 -2
  113. package/document-management/server/services/document-file.service.js +1 -1
  114. package/errors/errors.localization.d.ts +2 -2
  115. package/errors/errors.localization.js +2 -2
  116. package/errors/index.d.ts +1 -0
  117. package/errors/index.js +1 -0
  118. package/errors/too-many-requests.error.d.ts +5 -0
  119. package/errors/too-many-requests.error.js +7 -0
  120. package/examples/api/authentication.js +5 -5
  121. package/examples/api/custom-authentication.js +4 -3
  122. package/file/server/mime-type.js +1 -1
  123. package/http/http-body.d.ts +1 -0
  124. package/http/http-body.js +3 -0
  125. package/image-service/imgproxy/imgproxy-image-service.d.ts +0 -1
  126. package/image-service/imgproxy/imgproxy-image-service.js +9 -27
  127. package/key-value-store/postgres/drizzle/{0000_shocking_slipstream.sql → 0000_moaning_calypso.sql} +1 -1
  128. package/key-value-store/postgres/drizzle/meta/0000_snapshot.json +2 -2
  129. package/key-value-store/postgres/drizzle/meta/_journal.json +2 -2
  130. package/lock/postgres/drizzle/{0000_busy_tattoo.sql → 0000_nappy_wraith.sql} +1 -1
  131. package/lock/postgres/drizzle/meta/0000_snapshot.json +2 -2
  132. package/lock/postgres/drizzle/meta/_journal.json +2 -2
  133. package/logger/formatters/json.js +1 -1
  134. package/logger/formatters/pretty-print.js +1 -1
  135. package/mail/drizzle/{0000_numerous_the_watchers.sql → 0000_cultured_quicksilver.sql} +2 -2
  136. package/mail/drizzle/meta/0000_snapshot.json +4 -4
  137. package/mail/drizzle/meta/_journal.json +2 -9
  138. package/notification/server/drizzle/{0000_wise_pyro.sql → 0000_new_tenebrous.sql} +6 -6
  139. package/notification/server/drizzle/meta/0000_snapshot.json +7 -7
  140. package/notification/server/drizzle/meta/_journal.json +2 -2
  141. package/notification/tests/notification-flow.test.js +1 -8
  142. package/notification/tests/notification-type.service.test.js +3 -3
  143. package/openid-connect/oidc.service.js +2 -3
  144. package/orm/data-types/common.js +1 -1
  145. package/orm/server/drizzle/schema-converter.js +9 -4
  146. package/orm/server/encryption.js +1 -1
  147. package/orm/server/module.d.ts +0 -1
  148. package/orm/server/module.js +0 -4
  149. package/orm/server/repository.d.ts +2 -1
  150. package/orm/server/repository.js +7 -10
  151. package/orm/tests/encryption.test.js +4 -6
  152. package/orm/tests/repository-extra-coverage.test.js +0 -2
  153. package/orm/tests/repository-regression.test.js +0 -3
  154. package/package.json +9 -8
  155. package/password/README.md +1 -1
  156. package/password/have-i-been-pwned.js +1 -1
  157. package/rate-limit/postgres/drizzle/{0000_watery_rage.sql → 0000_serious_sauron.sql} +1 -1
  158. package/rate-limit/postgres/drizzle/meta/0000_snapshot.json +2 -2
  159. package/rate-limit/postgres/drizzle/meta/_journal.json +2 -2
  160. package/rate-limit/postgres/postgres-rate-limiter.d.ts +1 -1
  161. package/rate-limit/postgres/postgres-rate-limiter.js +1 -1
  162. package/rate-limit/rate-limiter.d.ts +1 -1
  163. package/rpc/tests/rpc.integration.test.js +25 -31
  164. package/supports.d.ts +1 -0
  165. package/supports.js +1 -0
  166. package/task-queue/postgres/drizzle/{0000_faithful_daimon_hellstrom.sql → 0000_dark_ronan.sql} +5 -5
  167. package/task-queue/postgres/drizzle/meta/0000_snapshot.json +10 -10
  168. package/task-queue/postgres/drizzle/meta/_journal.json +2 -9
  169. package/task-queue/postgres/task-queue.js +2 -2
  170. package/task-queue/tests/coverage-enhancement.test.js +2 -2
  171. package/test/drizzle/{0000_natural_cannonball.sql → 0000_organic_gamora.sql} +2 -2
  172. package/test/drizzle/meta/0000_snapshot.json +3 -4
  173. package/test/drizzle/meta/_journal.json +2 -9
  174. package/testing/integration-setup.d.ts +7 -3
  175. package/testing/integration-setup.js +119 -96
  176. package/utils/alphabet.d.ts +1 -0
  177. package/utils/alphabet.js +1 -0
  178. package/utils/base32.d.ts +4 -0
  179. package/utils/base32.js +49 -0
  180. package/utils/base64.d.ts +0 -2
  181. package/utils/base64.js +6 -70
  182. package/utils/equals.d.ts +13 -3
  183. package/utils/equals.js +29 -9
  184. package/utils/index.d.ts +1 -2
  185. package/utils/index.js +1 -2
  186. package/utils/random.d.ts +1 -0
  187. package/utils/random.js +14 -8
  188. package/authentication/errors/secret-requirements.error.d.ts +0 -5
  189. package/authentication/models/authentication-credentials.model.d.ts +0 -10
  190. package/authentication/models/secret-check-result.model.d.ts +0 -3
  191. package/authentication/server/authentication-secret-requirements.validator.d.ts +0 -55
  192. package/authentication/tests/authentication-ancillary.service.test.js +0 -13
  193. package/authentication/tests/authentication-secret-requirements.validator.test.js +0 -29
  194. package/authentication/tests/secret-requirements.error.test.js +0 -14
  195. package/mail/drizzle/0001_married_tarantula.sql +0 -12
  196. package/mail/drizzle/meta/0001_snapshot.json +0 -69
  197. package/orm/server/tokens.d.ts +0 -1
  198. package/orm/server/tokens.js +0 -2
  199. package/task-queue/postgres/drizzle/0001_rapid_infant_terrible.sql +0 -16
  200. package/task-queue/postgres/drizzle/meta/0001_snapshot.json +0 -753
  201. package/test/drizzle/0001_closed_the_captain.sql +0 -2
  202. package/test/drizzle/meta/0001_snapshot.json +0 -117
  203. package/utils/cryptography.d.ts +0 -137
  204. package/utils/cryptography.js +0 -201
  205. /package/authentication/tests/{authentication-ancillary.service.test.d.ts → authentication-password-requirements.validator.test.d.ts} +0 -0
  206. /package/authentication/tests/{authentication-secret-requirements.validator.test.d.ts → brute-force-protection.test.d.ts} +0 -0
  207. /package/authentication/tests/{secret-requirements.error.test.d.ts → password-requirements.error.test.d.ts} +0 -0
@@ -8,38 +8,27 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
8
8
  var AuthenticationService_1;
9
9
  import { ActorType, Auditor, AuditOutcome } from '../../audit/index.js';
10
10
  import { NIL_UUID } from '../../constants.js';
11
- import { ForbiddenError } from '../../errors/forbidden.error.js';
12
- import { InvalidCredentialsError } from '../../errors/index.js';
13
- import { InvalidTokenError } from '../../errors/invalid-token.error.js';
14
- import { NotFoundError } from '../../errors/not-found.error.js';
15
- import { NotImplementedError } from '../../errors/not-implemented.error.js';
16
- import { afterResolve, inject, provide, Singleton } from '../../injector/index.js';
17
- import { KeyValueStore } from '../../key-value-store/key-value.store.js';
11
+ import { createJwtTokenString, deriveBytes, encodeTotpSecret, generateTotpRecoveryCodes, generateTotpSecret, generateTotpUri, hashTotpRecoveryCode, importHmacKey, importKey, injectDerivedCryptoKey, parseAndValidateJwtTokenString, verifyTotpRecoveryCode, verifyTotpToken } from '../../cryptography/index.js';
12
+ import { ForbiddenError, InvalidCredentialsError, InvalidTokenError, NotFoundError, NotImplementedError } from '../../errors/index.js';
13
+ import { inject, provide, Singleton } from '../../injector/index.js';
18
14
  import { Logger } from '../../logger/logger.js';
19
15
  import { TRANSACTION_TIMESTAMP } from '../../orm/index.js';
20
16
  import { DatabaseConfig, injectRepository } from '../../orm/server/index.js';
21
17
  import { Alphabet } from '../../utils/alphabet.js';
22
18
  import { asyncHook } from '../../utils/async-hook/async-hook.js';
23
- import { decodeBase64, encodeBase64 } from '../../utils/base64.js';
24
- import { deriveBytesMultiple, importPbkdf2Key } from '../../utils/cryptography.js';
25
- import { currentTimestamp, timestampToTimestampSeconds } from '../../utils/date-time.js';
19
+ import { currentTimestamp, currentTimestampSeconds, timestampToTimestampSeconds } from '../../utils/date-time.js';
20
+ import { encodeUtf8 } from '../../utils/encoding.js';
26
21
  import { timingSafeBinaryEquals } from '../../utils/equals.js';
27
- import { createJwtTokenString } from '../../utils/jwt.js';
28
22
  import { isUuid } from '../../utils/patterns.js';
29
23
  import { getRandomBytes, getRandomString } from '../../utils/random.js';
30
- import { isBinaryData, isDefined, isString, isUndefined } from '../../utils/type-guards.js';
24
+ import { isDefined, isString, isUndefined } from '../../utils/type-guards.js';
31
25
  import { millisecondsPerDay, millisecondsPerHour, millisecondsPerMinute } from '../../utils/units.js';
32
- import { AuthenticationCredentials, AuthenticationSession, Subject, User } from '../models/index.js';
26
+ import { AuthenticationPassword, AuthenticationSession, AuthenticationTotp, AuthenticationTotpRecoveryCode, AuthenticationUsedTotpToken, Subject, SubjectStatus, TotpStatus, User } from '../models/index.js';
33
27
  import { AuthenticationAncillaryService, GetTokenPayloadContextAction } from './authentication-ancillary.service.js';
34
- import { AuthenticationSecretRequirementsValidator } from './authentication-secret-requirements.validator.js';
35
- import { getRefreshTokenFromString, getSecretResetTokenFromString, getTokenFromString } from './helper.js';
28
+ import { AuthenticationPasswordRequirementsValidator } from './authentication-password-requirements.validator.js';
29
+ import { getPasswordResetTokenFromString, getRefreshTokenFromString, getTokenFromString } from './helper.js';
36
30
  import { AuthenticationModuleConfig } from './module.js';
37
31
  export class AuthenticationServiceOptions {
38
- /**
39
- * Secrets used for signing tokens and refreshTokens.
40
- * If single secret is provided, multiple secrets are derived internally.
41
- */
42
- secret;
43
32
  /**
44
33
  * Token version, forces refresh on mismatch (useful if payload changes).
45
34
  *
@@ -65,112 +54,114 @@ export class AuthenticationServiceOptions {
65
54
  */
66
55
  rememberRefreshTokenTimeToLive;
67
56
  /**
68
- * How long a secret reset token is valid in milliseconds.
57
+ * How long a password reset token is valid in milliseconds.
69
58
  *
70
59
  * @default 10 minutes
71
60
  */
72
- secretResetTokenTimeToLive;
61
+ passwordResetTokenTimeToLive;
73
62
  /**
74
- * Number of iterations for password hashing.
63
+ * How long a TOTP challenge token is valid in milliseconds.
75
64
  *
76
- * @default 250000
65
+ * @default 5 minutes
77
66
  */
78
- hashIterations;
67
+ totpChallengeTokenTimeToLive;
79
68
  /**
80
- * Number of iterations for signing secrets derivation.
81
- *
82
- * @default 500000
69
+ * Options for brute force protection.
83
70
  */
84
- signingSecretsDerivationIterations;
71
+ bruteForceProtection;
72
+ /**
73
+ * TOTP issuer name.
74
+ */
75
+ totpIssuer;
76
+ /**
77
+ * Options for password hashing.
78
+ */
79
+ passwordHashing;
80
+ /**
81
+ * Options for TOTP.
82
+ */
83
+ totp;
85
84
  }
86
- const HASH_ITERATIONS = 250000;
87
85
  const HASH_LENGTH_BITS = 512;
88
86
  const HASH_LENGTH_BYTES = HASH_LENGTH_BITS / 8;
89
87
  const JWT_ID_LENGTH = 24;
90
88
  const REFRESH_TOKEN_SECRET_LENGTH = 64;
91
89
  const SALT_LENGTH = 32;
92
- const SIGNING_SECRETS_DERIVATION_ITERATIONS = 500000;
93
- const SIGNING_SECRETS_LENGTH = 64;
90
+ export const DEFAULT_TOTP_OPTIONS = {
91
+ codeHashAlgorithm: 'SHA-512',
92
+ recoveryCodeHashOptions: {
93
+ algorithm: {
94
+ name: 'Argon2id',
95
+ memory: 128 * 1024, // 128 MiB
96
+ parallelism: 4,
97
+ passes: 3,
98
+ },
99
+ length: 64,
100
+ },
101
+ };
102
+ const timingSafeFallbackPassword = { salt: new Uint8Array(SALT_LENGTH), hash: new Uint8Array(HASH_LENGTH_BYTES) };
94
103
  /**
95
104
  * Handles authentication on server side.
96
105
  *
97
- * Can be used to:
98
- * - Set credentials
99
- * - Authenticate
100
- * - Get token
101
- * - End session
102
- * - Refresh token
103
- * - Impersonate/unimpersonate
104
- * - Reset secret
105
- * - Check secret
106
- *
107
106
  * @template AdditionalTokenPayload Type of additional token payload
108
107
  * @template AuthenticationData Type of additional authentication data
109
- * @template AdditionalInitSecretResetData Type of additional secret reset data
108
+ * @template AdditionalInitPasswordResetData Type of additional password reset data
110
109
  */
111
110
  let AuthenticationService = AuthenticationService_1 = class AuthenticationService {
112
- #credentialsRepository = injectRepository(AuthenticationCredentials);
111
+ #passwordRepository = injectRepository(AuthenticationPassword);
113
112
  #sessionRepository = injectRepository(AuthenticationSession);
114
113
  #subjectRepository = injectRepository(Subject);
115
114
  #userRepository = injectRepository(User);
116
- #authenticationSecretRequirementsValidator = inject(AuthenticationSecretRequirementsValidator);
115
+ #totpRepository = injectRepository(AuthenticationTotp);
116
+ #totpRecoveryCodeRepository = injectRepository(AuthenticationTotpRecoveryCode);
117
+ #usedTotpTokenRepository = injectRepository(AuthenticationUsedTotpToken);
118
+ #authenticationPasswordRequirementsValidator = inject(AuthenticationPasswordRequirementsValidator);
117
119
  #authenticationAncillaryService = inject(AuthenticationAncillaryService, undefined, { optional: true });
118
- #keyValueStore = inject((KeyValueStore), 'authentication');
119
- #options = inject(AuthenticationServiceOptions);
120
+ #options = inject(AuthenticationServiceOptions, undefined, { optional: true }) ?? {};
120
121
  #logger = inject(Logger, 'Authentication');
122
+ #tokenSigningKey = injectDerivedCryptoKey('authentication:token-signing', { name: 'KMAC256' }, ['sign', 'verify']);
123
+ #refreshTokenSigningKey = injectDerivedCryptoKey('authentication:refresh-token-signing', { name: 'KMAC256' }, ['sign', 'verify']);
124
+ #passwordResetTokenSigningKey = injectDerivedCryptoKey('authentication:password-reset-token-signing', { name: 'KMAC256' }, ['sign', 'verify']);
125
+ #totpChallengeSigningKey = injectDerivedCryptoKey('authentication:totp-challenge-signing', { name: 'KMAC256' }, ['sign', 'verify']);
121
126
  hooks = {
122
127
  beforeLogin: asyncHook(),
123
128
  afterLogin: asyncHook(),
124
- beforeChangeSecret: asyncHook(),
125
- afterChangeSecret: asyncHook(),
129
+ beforeChangePassword: asyncHook(),
130
+ afterChangePassword: asyncHook(),
126
131
  };
127
132
  tokenVersion = this.#options.version ?? 1;
128
133
  tokenTimeToLive = this.#options.tokenTimeToLive ?? (5 * millisecondsPerMinute);
129
134
  refreshTokenTimeToLive = this.#options.refreshTokenTimeToLive ?? (6 * millisecondsPerHour);
130
135
  rememberRefreshTokenTimeToLive = this.#options.rememberRefreshTokenTimeToLive ?? (30 * millisecondsPerDay);
131
- secretResetTokenTimeToLive = this.#options.secretResetTokenTimeToLive ?? (10 * millisecondsPerMinute);
132
- derivedTokenSigningSecret;
133
- derivedRefreshTokenSigningSecret;
134
- derivedSecretResetTokenSigningSecret;
135
- /** @internal */
136
- async [afterResolve]() {
137
- await this.initialize();
138
- }
139
- /**
140
- * Initializes the service.
141
- * Derives signing secrets if necessary.
142
- *
143
- * @internal
144
- */
145
- async initialize() {
146
- if (isString(this.#options.secret) || isBinaryData(this.#options.secret)) {
147
- await this.deriveSigningSecrets(this.#options.secret);
148
- }
149
- else {
150
- this.derivedTokenSigningSecret = this.#options.secret.tokenSigningSecret;
151
- this.derivedRefreshTokenSigningSecret = this.#options.secret.refreshTokenSigningSecret;
152
- this.derivedSecretResetTokenSigningSecret = this.#options.secret.secretResetTokenSigningSecret;
153
- }
136
+ passwordResetTokenTimeToLive = this.#options.passwordResetTokenTimeToLive ?? (10 * millisecondsPerMinute);
137
+ totpOptions = this.#options.totp ?? DEFAULT_TOTP_OPTIONS;
138
+ hashDeriveOptions = this.#options.passwordHashing?.algorithm ?? {
139
+ name: 'Argon2id',
140
+ memory: 128 * 1024, // 128 MiB
141
+ parallelism: 4,
142
+ passes: 3,
143
+ };
144
+ getTotpOptions() {
145
+ return this.totpOptions;
154
146
  }
155
147
  /**
156
- * Sets the credentials for a subject.
157
- * This method should not be exposed to the public API without an authenticated current password or secret reset token check.
158
- * @param subject The subject to set the credentials for.
159
- * @param secret The secret to set.
160
- * @param options Options for setting the credentials.
148
+ * Sets the password for a subject.
149
+ * This method should not be exposed to the public API without an authenticated current password or password reset token check.
150
+ * @param subject The subject to set the password for.
151
+ * @param password The password to set.
152
+ * @param options Options for setting the password.
161
153
  */
162
- async setCredentials(subject, secret, options) {
163
- // We do not need to avoid information leakage here, as this is a non-public method that is only called by a public api if the secret reset token is valid.
154
+ async setPassword(subject, password, options) {
155
+ // We do not need to avoid information leakage here, as this is a non-public method that is only called by a public api if the password reset token is valid.
164
156
  if (options?.skipValidation != true) {
165
- await this.#authenticationSecretRequirementsValidator.validateSecretRequirements(secret);
157
+ await this.#authenticationPasswordRequirementsValidator.validatePasswordRequirements(password);
166
158
  }
167
159
  const salt = getRandomBytes(SALT_LENGTH);
168
- const hash = await this.getHash(secret, salt);
169
- await this.#credentialsRepository.transaction(async (tx) => {
170
- await this.#credentialsRepository.withTransaction(tx).upsert(['tenantId', 'subjectId'], {
160
+ const hash = await this.getHash(password, salt);
161
+ await this.#passwordRepository.transaction(async (tx) => {
162
+ await this.#passwordRepository.withTransaction(tx).upsert(['tenantId', 'subjectId'], {
171
163
  tenantId: subject.tenantId,
172
164
  subjectId: subject.id,
173
- hashVersion: 1,
174
165
  salt,
175
166
  hash,
176
167
  });
@@ -180,28 +171,39 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
180
171
  });
181
172
  }
182
173
  /**
183
- * Authenticates a subject with a secret.
174
+ * Authenticates a subject with a password.
184
175
  * @param subject The subject to authenticate.
185
- * @param secret The secret to authenticate with.
176
+ * @param password The password to authenticate with.
186
177
  * @returns The result of the authentication.
187
178
  */
188
- async authenticate(subject, secret) {
179
+ async authenticateWithPassword(subject, password) {
189
180
  const actualSubject = await this.tryResolveSubject(subject);
190
181
  // we use a random uuid instead of null here to reduce timing attack surface by DB optimiziations
191
182
  const queryTenantId = actualSubject?.tenantId ?? crypto.randomUUID();
192
183
  const querySubjectId = actualSubject?.id ?? crypto.randomUUID();
193
- const loadedCredentials = await this.#credentialsRepository.tryLoadByQuery({
184
+ const loadedPassword = await this.#passwordRepository.tryLoadByQuery({
194
185
  tenantId: queryTenantId,
195
186
  subjectId: querySubjectId,
196
187
  });
197
- const credentials = loadedCredentials ?? { salt: new Uint8Array(SALT_LENGTH), hash: new Uint8Array(HASH_LENGTH_BYTES) };
198
- const hash = await this.getHash(secret, credentials.salt);
199
- const valid = timingSafeBinaryEquals(hash, credentials.hash);
200
- if (valid && isDefined(actualSubject) && isDefined(loadedCredentials)) {
188
+ const passwordRecord = loadedPassword ?? timingSafeFallbackPassword;
189
+ const hash = await this.getHash(password, passwordRecord.salt);
190
+ const valid = timingSafeBinaryEquals(hash, passwordRecord.hash);
191
+ const subjectActive = isDefined(actualSubject) && (actualSubject.status == SubjectStatus.Active);
192
+ if (valid && subjectActive && isDefined(actualSubject) && isDefined(loadedPassword)) {
201
193
  return { success: true, subject: actualSubject };
202
194
  }
203
195
  return { success: false };
204
196
  }
197
+ /**
198
+ * Ensures that a subject is not suspended.
199
+ * @param subject The subject to check.
200
+ * @throws {ForbiddenError} If the subject is suspended.
201
+ */
202
+ ensureNotSuspended(subject) {
203
+ if (subject.status == SubjectStatus.Suspended) {
204
+ throw new ForbiddenError('Subject is suspended.');
205
+ }
206
+ }
205
207
  /**
206
208
  * Gets a token for a subject.
207
209
  * @param subject The subject to get the token for.
@@ -219,7 +221,6 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
219
221
  subjectId: subject.id,
220
222
  begin: now,
221
223
  end,
222
- refreshTokenHashVersion: 0,
223
224
  refreshTokenSalt: new Uint8Array(),
224
225
  refreshTokenHash: new Uint8Array(),
225
226
  });
@@ -228,7 +229,6 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
228
229
  const refreshToken = await this.createRefreshToken(subject, session.id, end, { impersonator, remember });
229
230
  await this.#sessionRepository.withTransaction(tx).update(session.id, {
230
231
  end,
231
- refreshTokenHashVersion: 1,
232
232
  refreshTokenSalt: refreshToken.salt,
233
233
  refreshTokenHash: refreshToken.hash,
234
234
  });
@@ -238,40 +238,52 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
238
238
  /**
239
239
  * Logs in a subject.
240
240
  * @param subjectInput The subject to log in.
241
- * @param secret The secret to log in with.
241
+ * @param password The password to log in with.
242
242
  * @param data Additional authentication data.
243
243
  * @param auditor Auditor for auditing.
244
244
  * @param remember Whether to remember the session.
245
- * @returns Token
245
+ * @returns Token or TOTP challenge.
246
246
  */
247
- async login(subjectInput, secret, data, auditor, remember = false) {
247
+ async login(subjectInput, password, data, auditor, remember = false) {
248
248
  const authAuditor = auditor.fork(AuthenticationService_1.name);
249
- const authenticationResult = await this.authenticate(subjectInput, secret);
249
+ const authenticationResult = await this.authenticateWithPassword(subjectInput, password);
250
250
  if (!authenticationResult.success) {
251
251
  const actualSubject = await this.tryResolveSubject(subjectInput);
252
252
  await authAuditor.warn('login-failure', {
253
253
  actorType: ActorType.Anonymous,
254
254
  tenantId: actualSubject?.tenantId ?? subjectInput.tenantId,
255
255
  targetId: actualSubject?.id ?? NIL_UUID,
256
- targetType: 'User',
256
+ targetType: 'Subject',
257
257
  details: { subjectInput, resolvedSubjectId: actualSubject?.id ?? null },
258
258
  });
259
259
  throw new InvalidCredentialsError();
260
260
  }
261
- await this.hooks.beforeLogin.trigger({ subject: authenticationResult.subject });
262
- const token = await this.getToken(authenticationResult.subject, data, { remember });
263
- await this.hooks.afterLogin.trigger({ subject: authenticationResult.subject });
261
+ const totp = await this.tryGetTotp(authenticationResult.subject.tenantId, authenticationResult.subject.id);
262
+ if (isDefined(totp) && totp.status == TotpStatus.Active) {
263
+ const challengeToken = await this.createTotpChallengeToken(authenticationResult.subject, data, remember);
264
+ return { type: 'totp', challengeToken };
265
+ }
266
+ return await this.#loginAlreadyValidatedSubject(authenticationResult.subject, data, authAuditor, remember);
267
+ }
268
+ async loginAlreadyValidatedSubject(subject, data, auditor, remember) {
269
+ const authAuditor = auditor.fork(AuthenticationService_1.name);
270
+ return await this.#loginAlreadyValidatedSubject(subject, data, authAuditor, remember);
271
+ }
272
+ async #loginAlreadyValidatedSubject(subject, data, authAuditor, remember) {
273
+ await this.hooks.beforeLogin.trigger({ subject });
274
+ const token = await this.getToken(subject, data, { remember });
275
+ await this.hooks.afterLogin.trigger({ subject });
264
276
  const sessionId = token.jsonToken.payload.session;
265
277
  await authAuditor.info('login-success', {
266
- tenantId: authenticationResult.subject.tenantId,
267
- actor: authenticationResult.subject.id,
278
+ tenantId: subject.tenantId,
279
+ actor: subject.id,
268
280
  actorType: ActorType.Subject,
269
- targetId: authenticationResult.subject.id,
270
- targetType: 'User',
281
+ targetId: subject.id,
282
+ targetType: 'Subject',
271
283
  network: { sessionId },
272
284
  details: { sessionId, remember },
273
285
  });
274
- return token;
286
+ return { type: 'success', result: token };
275
287
  }
276
288
  /**
277
289
  * Ends a session.
@@ -280,6 +292,9 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
280
292
  */
281
293
  async endSession(sessionId, auditor) {
282
294
  const authAuditor = auditor.fork(AuthenticationService_1.name);
295
+ await this.#endSession(sessionId, authAuditor);
296
+ }
297
+ async #endSession(sessionId, authAuditor) {
283
298
  const session = await this.#sessionRepository.tryLoad(sessionId);
284
299
  if (isUndefined(session)) {
285
300
  this.#logger.warn(`Session "${sessionId}" not found for logout.`);
@@ -292,7 +307,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
292
307
  actor: session.subjectId,
293
308
  actorType: ActorType.Subject,
294
309
  targetId: session.subjectId,
295
- targetType: 'User',
310
+ targetType: 'Subject',
296
311
  details: { sessionId },
297
312
  });
298
313
  }
@@ -304,13 +319,40 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
304
319
  */
305
320
  async invalidateAllSessions(tenantId, subjectId, auditor) {
306
321
  const authAuditor = auditor.fork(AuthenticationService_1.name);
322
+ return await this.#invalidateAllSessions(tenantId, subjectId, authAuditor);
323
+ }
324
+ async #invalidateAllSessions(tenantId, subjectId, authAuditor) {
307
325
  await this.#sessionRepository.updateManyByQuery({ tenantId, subjectId, end: { $gt: TRANSACTION_TIMESTAMP } }, { end: TRANSACTION_TIMESTAMP });
308
326
  await authAuditor.info('invalidate-all-sessions', {
309
327
  tenantId,
310
328
  actor: subjectId,
311
329
  actorType: ActorType.Subject,
312
330
  targetId: subjectId,
313
- targetType: 'User',
331
+ targetType: 'Subject',
332
+ });
333
+ }
334
+ /**
335
+ * Invalidates all sessions for a subject except the current one.
336
+ * @param tenantId The tenant id of the subject.
337
+ * @param subjectId The id of the subject.
338
+ * @param currentSessionId The id of the current session to keep.
339
+ * @param auditor Auditor for auditing.
340
+ */
341
+ async invalidateAllOtherSessions(tenantId, subjectId, currentSessionId, auditor) {
342
+ const authAuditor = auditor.fork(AuthenticationService_1.name);
343
+ await this.#invalidateAllOtherSessions(tenantId, subjectId, currentSessionId, authAuditor);
344
+ }
345
+ async #invalidateAllOtherSessions(tenantId, subjectId, currentSessionId, authAuditor) {
346
+ await this.#sessionRepository.updateManyByQuery({ id: { $neq: currentSessionId }, tenantId, subjectId, end: { $gt: TRANSACTION_TIMESTAMP } }, { end: TRANSACTION_TIMESTAMP });
347
+ await authAuditor.info('invalidate-all-other-sessions', {
348
+ tenantId,
349
+ actor: subjectId,
350
+ actorType: ActorType.Subject,
351
+ targetId: subjectId,
352
+ targetType: 'Subject',
353
+ details: {
354
+ currentSessionId,
355
+ },
314
356
  });
315
357
  }
316
358
  /**
@@ -320,7 +362,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
320
362
  * @returns List of sessions.
321
363
  */
322
364
  async listSessions(tenantId, subjectId) {
323
- return await this.#sessionRepository.loadManyByQuery({ tenantId, subjectId });
365
+ return await this.#sessionRepository.loadManyByQuery({ tenantId, subjectId, end: { $gt: TRANSACTION_TIMESTAMP } });
324
366
  }
325
367
  /**
326
368
  * Gets a session.
@@ -349,9 +391,25 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
349
391
  */
350
392
  async refresh(refreshToken, authenticationData, options = {}, auditor) {
351
393
  const authAuditor = auditor.fork(AuthenticationService_1.name);
394
+ const validatedRefreshToken = await this.validateRefreshToken(refreshToken);
395
+ return await this.#refreshAlreadyValidatedToken(validatedRefreshToken, authenticationData, options, authAuditor);
396
+ }
397
+ /**
398
+ * Refreshes a token.
399
+ * @param refreshToken The refresh token to use.
400
+ * @param authenticationData Additional authentication data.
401
+ * @param options Options for refreshing the token.
402
+ * @param auditor Auditor for auditing.
403
+ * @returns The token result.
404
+ * @throws {InvalidTokenError} If the refresh token is invalid.
405
+ */
406
+ async refreshAlreadyValidatedToken(validatedRefreshToken, authenticationData, options = {}, auditor) {
407
+ const authAuditor = auditor.fork(AuthenticationService_1.name);
408
+ return await this.#refreshAlreadyValidatedToken(validatedRefreshToken, authenticationData, options, authAuditor);
409
+ }
410
+ async #refreshAlreadyValidatedToken(validatedRefreshToken, authenticationData, options = {}, authAuditor) {
352
411
  let session;
353
412
  try {
354
- const validatedRefreshToken = await this.validateRefreshToken(refreshToken);
355
413
  const sessionId = validatedRefreshToken.payload.session;
356
414
  session = await this.#sessionRepository.load(sessionId);
357
415
  const hash = await this.getHash(validatedRefreshToken.payload.secret, session.refreshTokenSalt);
@@ -359,12 +417,12 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
359
417
  throw new InvalidTokenError('Session is expired.');
360
418
  }
361
419
  if (!timingSafeBinaryEquals(hash, session.refreshTokenHash)) {
362
- await this.endSession(sessionId, auditor);
420
+ await this.#endSession(sessionId, authAuditor);
363
421
  await authAuditor.warn('refresh-failure', {
364
422
  actorType: ActorType.Anonymous,
365
423
  tenantId: session.tenantId,
366
424
  targetId: session.subjectId,
367
- targetType: 'User',
425
+ targetType: 'Subject',
368
426
  details: { sessionId, reason: 'Token reuse detected. Session revoked.' },
369
427
  });
370
428
  throw new InvalidTokenError('Invalid refresh token.');
@@ -375,12 +433,12 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
375
433
  const ttl = remember ? this.rememberRefreshTokenTimeToLive : this.refreshTokenTimeToLive;
376
434
  const newEnd = now + ttl;
377
435
  const subject = await this.#subjectRepository.loadByQuery({ tenantId: session.tenantId, id: session.subjectId });
436
+ this.ensureNotSuspended(subject);
378
437
  const tokenPayload = await this.#authenticationAncillaryService?.getTokenPayload(subject, authenticationData, { action: GetTokenPayloadContextAction.Refresh });
379
438
  const { token, jsonToken } = await this.createToken({ additionalTokenPayload: tokenPayload, subject, sessionId, refreshTokenExpiration: newEnd, impersonator, timestamp: now });
380
439
  const newRefreshToken = await this.createRefreshToken(subject, sessionId, newEnd, { impersonator, remember });
381
440
  await this.#sessionRepository.update(sessionId, {
382
441
  end: newEnd,
383
- refreshTokenHashVersion: 1,
384
442
  refreshTokenSalt: newRefreshToken.salt,
385
443
  refreshTokenHash: newRefreshToken.hash,
386
444
  });
@@ -389,7 +447,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
389
447
  actor: session.subjectId,
390
448
  actorType: ActorType.Subject,
391
449
  targetId: session.subjectId,
392
- targetType: 'User',
450
+ targetType: 'Subject',
393
451
  details: { sessionId, remember },
394
452
  });
395
453
  return { token, jsonToken, refreshToken: newRefreshToken.token, remember, omitImpersonatorRefreshToken: options.omitImpersonator };
@@ -398,7 +456,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
398
456
  await authAuditor.warn('refresh-failure', {
399
457
  actorType: ActorType.Anonymous,
400
458
  targetId: session?.subjectId ?? NIL_UUID,
401
- targetType: 'User',
459
+ targetType: 'Subject',
402
460
  details: { sessionId: null, reason: error.message },
403
461
  });
404
462
  throw error;
@@ -427,12 +485,13 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
427
485
  actor: impersonatorSubject.id,
428
486
  actorType: ActorType.Subject,
429
487
  targetId: subjectId,
430
- targetType: 'User',
488
+ targetType: 'Subject',
431
489
  details: { impersonatedSubjectId: subjectId },
432
490
  });
433
491
  throw new ForbiddenError('Impersonation forbidden.');
434
492
  }
435
493
  const subject = await this.#subjectRepository.loadByQuery({ tenantId: validatedImpersonatorToken.payload.tenant, id: subjectId });
494
+ this.ensureNotSuspended(subject);
436
495
  const tokenResult = await this.getToken(subject, authenticationData, { impersonator: validatedImpersonatorToken.payload.subject });
437
496
  await authAuditor.info('impersonate-success', {
438
497
  tenantId: subject.tenantId,
@@ -441,7 +500,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
441
500
  impersonator: validatedImpersonatorToken.payload.subject,
442
501
  impersonatorType: ActorType.Subject,
443
502
  targetId: tokenResult.jsonToken.payload.subject,
444
- targetType: 'User',
503
+ targetType: 'Subject',
445
504
  details: { impersonatedSubjectId: subjectId },
446
505
  });
447
506
  return {
@@ -453,13 +512,22 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
453
512
  /**
454
513
  * Unimpersonates a subject.
455
514
  * @param impersonatorRefreshToken The refresh token of the impersonator.
515
+ * @param tokenString The token of the impersonated subject to end the session.
456
516
  * @param authenticationData Additional authentication data.
457
517
  * @param auditor Auditor for auditing.
458
518
  * @returns The token result.
459
519
  */
460
- async unimpersonate(impersonatorRefreshToken, authenticationData, auditor) {
520
+ async unimpersonate(impersonatorRefreshToken, tokenString, authenticationData, auditor) {
461
521
  const authAuditor = auditor.fork(AuthenticationService_1.name);
462
- const tokenResult = await this.refresh(impersonatorRefreshToken, authenticationData, { omitImpersonator: true }, auditor);
522
+ try {
523
+ const validatedToken = await this.validateToken(tokenString);
524
+ await this.#endSession(validatedToken.payload.session, authAuditor);
525
+ }
526
+ catch (error) {
527
+ this.#logger.warn(`Failed to end impersonated session: ${error.message}`);
528
+ }
529
+ const validatedRefreshToken = await this.validateRefreshToken(impersonatorRefreshToken);
530
+ const tokenResult = await this.#refreshAlreadyValidatedToken(validatedRefreshToken, authenticationData, { omitImpersonator: true }, authAuditor);
463
531
  await authAuditor.info('unimpersonate-success', {
464
532
  tenantId: tokenResult.jsonToken.payload.tenant,
465
533
  actor: tokenResult.jsonToken.payload.subject,
@@ -467,143 +535,157 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
467
535
  targetId: tokenResult.jsonToken.payload.subject,
468
536
  impersonatorType: ActorType.Subject,
469
537
  impersonator: tokenResult.jsonToken.payload.impersonator,
470
- targetType: 'User',
538
+ targetType: 'Subject',
471
539
  });
472
540
  return tokenResult;
473
541
  }
474
542
  /**
475
- * Initializes a secret reset. This usually involves sending an email for verification.
476
- * @param subject The subject to reset the secret for.
477
- * @param data Additional data for the secret reset.
543
+ * Initializes a password reset. This usually involves sending an email for verification.
544
+ * @param subject The subject to reset the password for.
545
+ * @param data Additional data for the password reset.
478
546
  * @param auditor Auditor for auditing.
479
547
  * @throws {NotImplementedError} If no ancillary service is registered.
480
548
  */
481
- async initSecretReset(subject, data, auditor) {
549
+ async initPasswordReset(subject, data, auditor) {
550
+ const authAuditor = auditor.fork(AuthenticationService_1.name);
551
+ await this.#initPasswordReset(subject, data, authAuditor);
552
+ }
553
+ async #initPasswordReset(subject, data, authAuditor) {
482
554
  if (isUndefined(this.#authenticationAncillaryService)) {
483
555
  throw new NotImplementedError('No ancillary service registered.');
484
556
  }
485
- const authAuditor = auditor.fork(AuthenticationService_1.name);
486
557
  const actualSubject = await this.tryResolveSubject(subject);
487
558
  if (isUndefined(actualSubject)) {
488
- this.#logger.warn(`Subject "${subject.subject}" not found for secret reset.`);
559
+ this.#logger.warn(`Subject "${subject.subject}" not found for password reset.`);
489
560
  /**
490
561
  * If the subject cannot be resolved, we do not throw an error here to avoid information leakage.
491
- * This is to prevent attackers from discovering valid subjects by trying to reset secrets.
562
+ * This is to prevent attackers from discovering valid subjects by trying to reset passwords.
492
563
  * Instead, we simply log the attempt and return without performing any action.
493
564
  */
494
565
  return;
495
566
  }
496
- const secretResetToken = await this.createSecretResetToken(actualSubject, currentTimestamp() + this.secretResetTokenTimeToLive);
497
- const initSecretResetData = {
567
+ if (actualSubject.status == SubjectStatus.Suspended) {
568
+ this.#logger.warn(`Subject "${actualSubject.id}" is suspended. Skipping password reset initialization.`);
569
+ return;
570
+ }
571
+ const passwordResetToken = await this.createPasswordResetToken(actualSubject, currentTimestamp() + this.passwordResetTokenTimeToLive);
572
+ const initPasswordResetData = {
498
573
  subject: actualSubject.id,
499
- token: secretResetToken.token,
574
+ token: passwordResetToken.token,
500
575
  ...data,
501
576
  };
502
- await this.#authenticationAncillaryService.handleInitSecretReset(initSecretResetData);
503
- await authAuditor.info('init-secret-reset', {
577
+ await this.#authenticationAncillaryService.handleInitPasswordReset(initPasswordResetData);
578
+ await authAuditor.info('init-password-reset', {
504
579
  tenantId: actualSubject.tenantId,
505
580
  targetId: actualSubject.id,
506
- targetType: 'User',
581
+ targetType: 'Subject',
507
582
  });
508
583
  }
509
584
  /**
510
- * Changes a subject's secret.
511
- * @param subjectInput The subject to change the secret for.
512
- * @param currentSecret The current secret.
513
- * @param newSecret The new secret.
585
+ * Changes a subject's password.
586
+ * @param subjectInput The subject to change the password for.
587
+ * @param currentPassword The current password.
588
+ * @param newPassword The new password.
514
589
  * @param auditor Auditor for auditing.
515
590
  */
516
- async changeSecret(subjectInput, currentSecret, newSecret, auditor) {
591
+ async changePassword(subjectInput, currentPassword, newPassword, auditor) {
517
592
  const authAuditor = auditor.fork(AuthenticationService_1.name);
518
- const authenticationResult = await this.authenticate(subjectInput, currentSecret);
593
+ await this.#changePassword(subjectInput, currentPassword, newPassword, authAuditor);
594
+ }
595
+ async #changePassword(subjectInput, currentPassword, newPassword, authAuditor) {
596
+ const authenticationResult = await this.authenticateWithPassword(subjectInput, currentPassword);
519
597
  if (!authenticationResult.success) {
520
598
  const resolvedSubject = await this.tryResolveSubject(subjectInput);
521
- await authAuditor.warn('change-secret-failure', {
599
+ await authAuditor.warn('change-password-failure', {
522
600
  actorType: ActorType.Anonymous,
523
601
  tenantId: resolvedSubject?.tenantId,
524
602
  targetId: resolvedSubject?.id ?? NIL_UUID,
525
- targetType: 'User',
603
+ targetType: 'Subject',
526
604
  details: { subjectInput, resolvedSubjectId: resolvedSubject?.id ?? null },
527
605
  });
528
606
  throw new InvalidCredentialsError();
529
607
  }
530
- await this.hooks.beforeChangeSecret.trigger({ subject: authenticationResult.subject });
531
- await this.setCredentials(authenticationResult.subject, newSecret);
532
- await this.hooks.afterChangeSecret.trigger({ subject: authenticationResult.subject });
533
- await authAuditor.info('change-secret-success', {
608
+ await this.hooks.beforeChangePassword.trigger({ subject: authenticationResult.subject });
609
+ await this.setPassword(authenticationResult.subject, newPassword);
610
+ await this.hooks.afterChangePassword.trigger({ subject: authenticationResult.subject });
611
+ await authAuditor.info('change-password-success', {
534
612
  tenantId: authenticationResult.subject.tenantId,
535
613
  actor: authenticationResult.subject.id,
536
614
  actorType: ActorType.Subject,
537
615
  targetId: authenticationResult.subject.id,
538
- targetType: 'User',
616
+ targetType: 'Subject',
539
617
  });
540
618
  }
541
619
  /**
542
- * Resets a secret.
543
- * @param tokenString The secret reset token.
544
- * @param newSecret The new secret.
620
+ * Resets a password.
621
+ * @param tokenString The password reset token.
622
+ * @param newPassword The new password.
545
623
  * @param auditor Auditor for auditing.
546
624
  * @throws {InvalidTokenError} If the token is invalid.
547
625
  */
548
- async resetSecret(tokenString, newSecret, auditor) {
626
+ async resetPassword(tokenString, newPassword, auditor) {
549
627
  const authAuditor = auditor.fork(AuthenticationService_1.name);
628
+ await this.#resetPassword(tokenString, newPassword, authAuditor);
629
+ }
630
+ async #resetPassword(tokenString, newPassword, authAuditor) {
550
631
  try {
551
- const token = await this.validateSecretResetToken(tokenString);
552
- const credentials = await this.#credentialsRepository.tryLoadByQuery({
632
+ const token = await this.validatePasswordResetToken(tokenString);
633
+ const passwordRecord = await this.#passwordRepository.tryLoadByQuery({
553
634
  tenantId: token.payload.tenant,
554
635
  subjectId: token.payload.subject,
555
636
  });
556
- if (isDefined(credentials)) {
557
- const lastUpdateSeconds = timestampToTimestampSeconds(credentials.metadata.revisionTimestamp);
637
+ if (isDefined(passwordRecord)) {
638
+ const lastUpdateSeconds = timestampToTimestampSeconds(passwordRecord.metadata.revisionTimestamp);
558
639
  if (token.payload.iat < lastUpdateSeconds) {
559
- await authAuditor.info('reset-secret-failure', {
640
+ await authAuditor.info('reset-password-failure', {
560
641
  targetId: token.payload.subject,
561
- targetType: 'User',
562
- details: { reason: 'Token is invalid (credentials have already been changed).' },
642
+ targetType: 'Subject',
643
+ details: { reason: 'Token is invalid (password has already been changed).' },
563
644
  });
564
645
  throw new InvalidTokenError();
565
646
  }
566
647
  }
567
648
  const subject = await this.#subjectRepository.loadByQuery({ tenantId: token.payload.tenant, id: token.payload.subject });
568
- await this.setCredentials(subject, newSecret);
569
- await authAuditor.info('reset-secret-success', {
649
+ this.ensureNotSuspended(subject);
650
+ await this.setPassword(subject, newPassword);
651
+ await authAuditor.info('reset-password-success', {
570
652
  tenantId: token.payload.tenant,
571
653
  targetId: token.payload.subject,
572
- targetType: 'User',
654
+ targetType: 'Subject',
573
655
  });
574
656
  }
575
657
  catch (error) {
576
- await authAuditor.warn('reset-secret-failure', {
658
+ await authAuditor.warn('reset-password-failure', {
577
659
  targetId: NIL_UUID,
578
- targetType: 'User',
660
+ targetType: 'Subject',
579
661
  details: { reason: error.message },
580
662
  });
581
663
  throw error;
582
664
  }
583
665
  }
584
666
  /**
585
- * Checks a secret against the requirements.
586
- * @param secret The secret to check.
667
+ * Checks a password against the requirements.
668
+ * @param password The password to check.
587
669
  * @returns The result of the check.
588
670
  */
589
- async checkSecret(secret) {
590
- return await this.#authenticationSecretRequirementsValidator.checkSecretRequirements(secret);
671
+ async checkPassword(password) {
672
+ return await this.#authenticationPasswordRequirementsValidator.checkPasswordRequirements(password);
591
673
  }
592
674
  /**
593
- * Tests a secret against the requirements.
594
- * @param secret The secret to test.
675
+ * Tests a password against the requirements.
676
+ * @param password The password to test.
595
677
  * @returns The result of the test.
596
678
  */
597
- async testSecret(secret) {
598
- return await this.#authenticationSecretRequirementsValidator.testSecretRequirements(secret);
679
+ async testPassword(password) {
680
+ return await this.#authenticationPasswordRequirementsValidator.testPasswordRequirements(password);
599
681
  }
600
682
  /**
601
- * Validates a secret against the requirements. Throws an error if the requirements are not met.
602
- * @param secret The secret to validate.
603
- * @throws {SecretRequirementsError} If the secret does not meet the requirements.
683
+ * Validates a password against the requirements. Throws an error if the requirements are not met.
684
+ * @param password The password to validate.
685
+ * @throws {PasswordRequirementsError} If the password does not meet the requirements.
604
686
  */
605
- async validateSecret(secret) {
606
- await this.#authenticationSecretRequirementsValidator.validateSecretRequirements(secret);
687
+ async validatePassword(password) {
688
+ await this.#authenticationPasswordRequirementsValidator.validatePasswordRequirements(password);
607
689
  }
608
690
  /**
609
691
  * Validates a token.
@@ -612,7 +694,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
612
694
  * @throws {InvalidTokenError} If the token is invalid.
613
695
  */
614
696
  async validateToken(token) {
615
- return await getTokenFromString(token, this.tokenVersion, this.derivedTokenSigningSecret);
697
+ return await getTokenFromString(token, this.tokenVersion, await this.#tokenSigningKey.getKey());
616
698
  }
617
699
  /**
618
700
  * Validates a refresh token.
@@ -621,16 +703,16 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
621
703
  * @throws {InvalidTokenError} If the refresh token is invalid.
622
704
  */
623
705
  async validateRefreshToken(token) {
624
- return await getRefreshTokenFromString(token, this.derivedRefreshTokenSigningSecret);
706
+ return await getRefreshTokenFromString(token, await this.#refreshTokenSigningKey.getKey());
625
707
  }
626
708
  /**
627
- * Validates a secret reset token.
628
- * @param token The secret reset token to validate.
629
- * @returns The validated secret reset token.
630
- * @throws {InvalidTokenError} If the secret reset token is invalid.
709
+ * Validates a password reset token.
710
+ * @param token The password reset token to validate.
711
+ * @returns The validated password reset token.
712
+ * @throws {InvalidTokenError} If the password reset token is invalid.
631
713
  */
632
- async validateSecretResetToken(token) {
633
- return await getSecretResetTokenFromString(token, this.derivedSecretResetTokenSigningSecret);
714
+ async validatePasswordResetToken(token) {
715
+ return await getPasswordResetTokenFromString(token, await this.#passwordResetTokenSigningKey.getKey());
634
716
  }
635
717
  /**
636
718
  * Tries to resolve a subject.
@@ -682,7 +764,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
682
764
  async createToken({ tokenVersion, jwtId, issuedAt, expiration, additionalTokenPayload, subject, sessionId, refreshTokenExpiration, impersonator: impersonatedBy, timestamp = currentTimestamp() }) {
683
765
  const header = {
684
766
  v: tokenVersion ?? this.tokenVersion,
685
- alg: 'HS256',
767
+ alg: 'KMAC256',
686
768
  typ: 'JWT',
687
769
  };
688
770
  const payload = {
@@ -700,7 +782,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
700
782
  header,
701
783
  payload,
702
784
  };
703
- const token = await createJwtTokenString(jsonToken, this.derivedTokenSigningSecret);
785
+ const token = await createJwtTokenString(jsonToken, await this.#tokenSigningKey.getKey());
704
786
  return { token, jsonToken };
705
787
  }
706
788
  /**
@@ -718,7 +800,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
718
800
  const hash = await this.getHash(secret, salt);
719
801
  const jsonToken = {
720
802
  header: {
721
- alg: 'HS256',
803
+ alg: 'KMAC256',
722
804
  typ: 'JWT',
723
805
  },
724
806
  payload: {
@@ -731,7 +813,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
731
813
  secret,
732
814
  },
733
815
  };
734
- const token = await createJwtTokenString(jsonToken, this.derivedRefreshTokenSigningSecret);
816
+ const token = await createJwtTokenString(jsonToken, await this.#refreshTokenSigningKey.getKey());
735
817
  return { token, jsonToken, salt, hash: new Uint8Array(hash) };
736
818
  }
737
819
  async defaultResolveSubjects({ tenantId, subject }) {
@@ -751,11 +833,11 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
751
833
  }
752
834
  return subjects[0];
753
835
  }
754
- async createSecretResetToken(subject, expirationTimestamp) {
755
- const iat = timestampToTimestampSeconds(currentTimestamp());
836
+ async createPasswordResetToken(subject, expirationTimestamp) {
837
+ const iat = currentTimestampSeconds();
756
838
  const jsonToken = {
757
839
  header: {
758
- alg: 'HS256',
840
+ alg: 'KMAC256',
759
841
  typ: 'JWT',
760
842
  },
761
843
  payload: {
@@ -765,23 +847,363 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
765
847
  tenant: subject.tenantId,
766
848
  },
767
849
  };
768
- const token = await createJwtTokenString(jsonToken, this.derivedSecretResetTokenSigningSecret);
850
+ const token = await createJwtTokenString(jsonToken, await this.#passwordResetTokenSigningKey.getKey());
769
851
  return { token, jsonToken };
770
852
  }
771
- async deriveSigningSecrets(secret) {
772
- const key = await importPbkdf2Key(secret);
773
- const saltBase64 = await this.#keyValueStore.getOrSet('derivationSalt', encodeBase64(getRandomBytes(SALT_LENGTH)));
774
- const salt = decodeBase64(saltBase64);
775
- const algorithm = { name: 'PBKDF2', hash: 'SHA-512', iterations: this.#options.signingSecretsDerivationIterations ?? SIGNING_SECRETS_DERIVATION_ITERATIONS, salt };
776
- const [derivedTokenSigningSecret, derivedRefreshTokenSigningSecret, derivedSecretResetTokenSigningSecret] = await deriveBytesMultiple(algorithm, key, 3, SIGNING_SECRETS_LENGTH);
777
- this.derivedTokenSigningSecret = derivedTokenSigningSecret;
778
- this.derivedRefreshTokenSigningSecret = derivedRefreshTokenSigningSecret;
779
- this.derivedSecretResetTokenSigningSecret = derivedSecretResetTokenSigningSecret;
780
- }
781
- async getHash(secret, salt) {
782
- const key = await importPbkdf2Key(secret);
783
- const hash = await globalThis.crypto.subtle.deriveBits({ name: 'PBKDF2', hash: 'SHA-512', iterations: this.#options.hashIterations ?? HASH_ITERATIONS, salt }, key, HASH_LENGTH_BITS);
784
- return new Uint8Array(hash);
853
+ async getHash(password, salt) {
854
+ const keyData = isString(password) ? encodeUtf8(password) : password;
855
+ const derviveAlgorithm = { ...this.hashDeriveOptions, nonce: salt };
856
+ const key = await importKey('raw-secret', keyData, derviveAlgorithm, false, ['deriveBits']);
857
+ return await deriveBytes(derviveAlgorithm, key, HASH_LENGTH_BYTES);
858
+ }
859
+ async tryGetTotp(tenantId, subjectId) {
860
+ return await this.#totpRepository.tryLoadByQuery({ tenantId, subjectId });
861
+ }
862
+ async getTotpStatus(tenantId, subjectId) {
863
+ const totp = await this.tryGetTotp(tenantId, subjectId);
864
+ return { active: totp?.status == TotpStatus.Active };
865
+ }
866
+ async initEnrollTotp(tenantId, subjectId, auditor) {
867
+ const authAuditor = auditor.fork(AuthenticationService_1.name);
868
+ return await this.#initEnrollTotp(tenantId, subjectId, authAuditor);
869
+ }
870
+ async #initEnrollTotp(tenantId, subjectId, authAuditor) {
871
+ const subject = await this.#subjectRepository.loadByQuery({ tenantId, id: subjectId });
872
+ const existingTotp = await this.tryGetTotp(tenantId, subjectId);
873
+ if (isDefined(existingTotp) && existingTotp.status == TotpStatus.Active) {
874
+ throw new ForbiddenError('TOTP already active');
875
+ }
876
+ const secret = generateTotpSecret(this.totpOptions.codeHashAlgorithm);
877
+ const recoveryCodeSalt = getRandomBytes(16);
878
+ const issuer = this.#options.totpIssuer ?? 'tstdl';
879
+ const user = await this.#userRepository.tryLoadByQuery({ tenantId, id: subjectId });
880
+ const accountName = user?.email ?? subjectId;
881
+ const encodedSecret = encodeTotpSecret(secret);
882
+ const uri = generateTotpUri(encodedSecret, accountName, issuer, this.totpOptions);
883
+ if (isDefined(existingTotp)) {
884
+ await this.#totpRepository.updateByQuery({ tenantId, id: existingTotp.id }, {
885
+ secret,
886
+ recoveryCodeSalt,
887
+ status: TotpStatus.Pending,
888
+ });
889
+ }
890
+ else {
891
+ await this.#totpRepository.insert({
892
+ tenantId,
893
+ subjectId,
894
+ secret,
895
+ recoveryCodeSalt,
896
+ status: TotpStatus.Pending,
897
+ });
898
+ }
899
+ await authAuditor.info('totp-enroll-init', {
900
+ tenantId,
901
+ actor: subjectId,
902
+ actorType: ActorType.Subject,
903
+ targetId: subjectId,
904
+ targetType: 'Subject',
905
+ details: { subjectId },
906
+ });
907
+ return { secret: encodedSecret, uri };
908
+ }
909
+ async completeEnrollTotp(tenantId, subjectId, token, auditor) {
910
+ const authAuditor = auditor.fork(AuthenticationService_1.name);
911
+ return await this.#completeEnrollTotp(tenantId, subjectId, token, authAuditor);
912
+ }
913
+ async #completeEnrollTotp(tenantId, subjectId, token, authAuditor) {
914
+ const totp = await this.#totpRepository.loadByQuery({ tenantId, subjectId });
915
+ if (totp.status == TotpStatus.Active) {
916
+ throw new ForbiddenError('TOTP already active');
917
+ }
918
+ const isValid = await this.verifyAndRecordTotpToken(totp, token, authAuditor);
919
+ if (!isValid) {
920
+ await authAuditor.warn('totp-enroll-failure', {
921
+ tenantId,
922
+ actor: subjectId,
923
+ actorType: ActorType.Subject,
924
+ targetId: subjectId,
925
+ targetType: 'Subject',
926
+ details: { reason: 'Invalid TOTP token' },
927
+ });
928
+ throw new ForbiddenError('Invalid TOTP token');
929
+ }
930
+ const recoveryCodes = generateTotpRecoveryCodes();
931
+ const hashingOptions = this.#getTotpRecoveryCodeHashingOptions(totp.recoveryCodeSalt);
932
+ const hashedRecoveryCodes = await Promise.all(recoveryCodes.map(async (code) => {
933
+ const hashedCode = await hashTotpRecoveryCode(code, hashingOptions);
934
+ return {
935
+ tenantId,
936
+ totpId: totp.id,
937
+ code: hashedCode,
938
+ usedTimestamp: null,
939
+ };
940
+ }));
941
+ await this.#totpRepository.transaction(async (tx) => {
942
+ await this.#totpRepository.withTransaction(tx).updateByQuery({ tenantId, id: totp.id }, { status: TotpStatus.Active });
943
+ await this.#totpRecoveryCodeRepository.withTransaction(tx).insertMany(hashedRecoveryCodes);
944
+ });
945
+ await authAuditor.info('totp-enroll-success', {
946
+ tenantId,
947
+ actor: subjectId,
948
+ actorType: ActorType.Subject,
949
+ targetId: subjectId,
950
+ targetType: 'Subject',
951
+ });
952
+ return { recoveryCodes };
953
+ }
954
+ async disableTotp(tenantId, subjectId, token, auditor) {
955
+ const authAuditor = auditor.fork(AuthenticationService_1.name);
956
+ await this.#disableTotp(tenantId, subjectId, token, authAuditor);
957
+ }
958
+ async disableTotpWithRecoveryCode(tenantId, subjectId, recoveryCode, auditor) {
959
+ const authAuditor = auditor.fork(AuthenticationService_1.name);
960
+ await this.#disableTotpWithRecoveryCode(tenantId, subjectId, recoveryCode, authAuditor);
961
+ }
962
+ async regenerateRecoveryCodes(tenantId, subjectId, token, auditor, options) {
963
+ const authAuditor = auditor.fork(AuthenticationService_1.name);
964
+ return await this.#regenerateRecoveryCodes(tenantId, subjectId, token, authAuditor, options);
965
+ }
966
+ async #regenerateRecoveryCodes(tenantId, subjectId, token, authAuditor, options) {
967
+ const totp = await this.#totpRepository.loadByQuery({ tenantId, subjectId });
968
+ if (totp.status != TotpStatus.Active) {
969
+ throw new ForbiddenError('TOTP not active');
970
+ }
971
+ const isValid = await this.verifyAndRecordTotpToken(totp, token, authAuditor);
972
+ if (!isValid) {
973
+ await authAuditor.warn('totp-verify-failure', {
974
+ tenantId,
975
+ actor: subjectId,
976
+ actorType: ActorType.Subject,
977
+ targetId: subjectId,
978
+ targetType: 'Subject',
979
+ details: { reason: 'Invalid TOTP token' },
980
+ });
981
+ throw new ForbiddenError('Invalid TOTP token');
982
+ }
983
+ const recoveryCodes = generateTotpRecoveryCodes();
984
+ const hashingOptions = this.#getTotpRecoveryCodeHashingOptions(totp.recoveryCodeSalt);
985
+ const hashedRecoveryCodes = await Promise.all(recoveryCodes.map(async (code) => {
986
+ const hashedCode = await hashTotpRecoveryCode(code, hashingOptions);
987
+ return {
988
+ tenantId,
989
+ totpId: totp.id,
990
+ code: hashedCode,
991
+ usedTimestamp: null,
992
+ };
993
+ }));
994
+ await this.#totpRepository.transaction(async (transaction) => {
995
+ const totpRecoveryCodeRepository = this.#totpRecoveryCodeRepository.withTransaction(transaction);
996
+ await totpRecoveryCodeRepository.deleteByQuery({ tenantId, totpId: totp.id });
997
+ await totpRecoveryCodeRepository.insertMany(hashedRecoveryCodes);
998
+ if (options?.invalidateOtherSessions == true) {
999
+ await this.#invalidateAllOtherSessions(tenantId, subjectId, NIL_UUID, authAuditor);
1000
+ }
1001
+ });
1002
+ await authAuditor.info('recovery-codes-regenerated', {
1003
+ tenantId,
1004
+ actor: subjectId,
1005
+ actorType: ActorType.Subject,
1006
+ targetId: subjectId,
1007
+ targetType: 'Subject',
1008
+ details: { count: recoveryCodes.length },
1009
+ });
1010
+ return { recoveryCodes };
1011
+ }
1012
+ async #disableTotp(tenantId, subjectId, token, authAuditor) {
1013
+ const totp = await this.#totpRepository.loadByQuery({ tenantId, subjectId });
1014
+ if (totp.status != TotpStatus.Active) {
1015
+ throw new ForbiddenError('TOTP not active');
1016
+ }
1017
+ const isValid = await this.verifyAndRecordTotpToken(totp, token, authAuditor);
1018
+ if (!isValid) {
1019
+ await authAuditor.warn('totp-disable-failure', {
1020
+ tenantId,
1021
+ actor: subjectId,
1022
+ actorType: ActorType.Subject,
1023
+ targetId: subjectId,
1024
+ targetType: 'Subject',
1025
+ details: { reason: 'Invalid token' },
1026
+ });
1027
+ throw new ForbiddenError('Invalid TOTP token');
1028
+ }
1029
+ await this.#totpRepository.deleteByQuery({ tenantId, id: totp.id });
1030
+ await authAuditor.info('totp-disable-success', {
1031
+ tenantId,
1032
+ actor: subjectId,
1033
+ actorType: ActorType.Subject,
1034
+ targetId: subjectId,
1035
+ targetType: 'Subject',
1036
+ });
1037
+ }
1038
+ async #disableTotpWithRecoveryCode(tenantId, subjectId, recoveryCode, authAuditor) {
1039
+ const totp = await this.#totpRepository.loadByQuery({ tenantId, subjectId });
1040
+ if (totp.status != TotpStatus.Active) {
1041
+ throw new ForbiddenError('TOTP not active');
1042
+ }
1043
+ const isRecoveryCodeValid = await this.verifyAndUseRecoveryCode(totp, recoveryCode);
1044
+ if (!isRecoveryCodeValid) {
1045
+ await authAuditor.warn('totp-disable-failure', {
1046
+ tenantId,
1047
+ actor: subjectId,
1048
+ actorType: ActorType.Subject,
1049
+ targetId: subjectId,
1050
+ targetType: 'Subject',
1051
+ details: { reason: 'Invalid recovery code' },
1052
+ });
1053
+ throw new ForbiddenError('Invalid recovery code');
1054
+ }
1055
+ await this.#totpRepository.deleteByQuery({ tenantId, id: totp.id });
1056
+ await authAuditor.info('totp-disable-success', {
1057
+ tenantId,
1058
+ actor: subjectId,
1059
+ actorType: ActorType.Subject,
1060
+ targetId: subjectId,
1061
+ targetType: 'Subject',
1062
+ });
1063
+ }
1064
+ async loginVerifyTotp(challengeTokenString, token, auditor) {
1065
+ const authAuditor = auditor.fork(AuthenticationService_1.name);
1066
+ return await this.#loginVerifyTotp(challengeTokenString, token, authAuditor);
1067
+ }
1068
+ async loginRecovery(challengeTokenString, recoveryCode, auditor) {
1069
+ const authAuditor = auditor.fork(AuthenticationService_1.name);
1070
+ return await this.#loginRecovery(challengeTokenString, recoveryCode, authAuditor);
1071
+ }
1072
+ async #loginRecovery(challengeTokenString, recoveryCode, authAuditor) {
1073
+ const challengeToken = await this.validateTotpChallengeToken(challengeTokenString);
1074
+ const { tenant, subject: subjectId, remember, data } = challengeToken.payload;
1075
+ const totp = await this.#totpRepository.loadByQuery({ tenantId: tenant, subjectId });
1076
+ const isRecoveryCodeValid = await this.verifyAndUseRecoveryCode(totp, recoveryCode);
1077
+ if (!isRecoveryCodeValid) {
1078
+ await authAuditor.warn('recovery-login-failure', {
1079
+ actorType: ActorType.Anonymous,
1080
+ tenantId: tenant,
1081
+ targetId: subjectId,
1082
+ targetType: 'Subject',
1083
+ details: {
1084
+ subjectInput: { tenantId: tenant, subject: subjectId },
1085
+ resolvedSubjectId: subjectId,
1086
+ },
1087
+ });
1088
+ throw new ForbiddenError('Invalid recovery code');
1089
+ }
1090
+ const subject = await this.#subjectRepository.loadByQuery({ tenantId: tenant, id: subjectId });
1091
+ const loginResult = await this.#loginAlreadyValidatedSubject(subject, data, authAuditor, remember);
1092
+ const unusedRecoveryCodesCount = await this.#totpRecoveryCodeRepository.countByQuery({ tenantId: tenant, totpId: totp.id, usedTimestamp: null });
1093
+ await authAuditor.info('recovery-login-success', {
1094
+ tenantId: tenant,
1095
+ actor: subjectId,
1096
+ actorType: ActorType.Subject,
1097
+ targetId: subjectId,
1098
+ targetType: 'Subject',
1099
+ network: { sessionId: loginResult.result.jsonToken.payload.session },
1100
+ details: {
1101
+ sessionId: loginResult.result.jsonToken.payload.session,
1102
+ remember,
1103
+ },
1104
+ });
1105
+ return {
1106
+ ...loginResult,
1107
+ lowRecoveryCodesWarning: unusedRecoveryCodesCount <= 3,
1108
+ };
1109
+ }
1110
+ async #loginVerifyTotp(challengeTokenString, token, authAuditor) {
1111
+ const challengeToken = await this.validateTotpChallengeToken(challengeTokenString);
1112
+ const { tenant, subject: subjectId, remember, data } = challengeToken.payload;
1113
+ const totp = await this.#totpRepository.loadByQuery({ tenantId: tenant, subjectId });
1114
+ const isValid = await this.verifyAndRecordTotpToken(totp, token, authAuditor);
1115
+ if (!isValid) {
1116
+ await authAuditor.warn('totp-verify-failure', {
1117
+ tenantId: tenant,
1118
+ actor: subjectId,
1119
+ actorType: ActorType.Subject,
1120
+ targetId: subjectId,
1121
+ targetType: 'Subject',
1122
+ details: { reason: 'Invalid token' },
1123
+ });
1124
+ throw new ForbiddenError('Invalid TOTP token');
1125
+ }
1126
+ const subject = await this.#subjectRepository.loadByQuery({ tenantId: tenant, id: subjectId });
1127
+ return await this.#loginAlreadyValidatedSubject(subject, data, authAuditor, remember);
1128
+ }
1129
+ async validateTotpChallengeToken(tokenString) {
1130
+ const validatedToken = await parseAndValidateJwtTokenString(tokenString, 'KMAC256', await this.#totpChallengeSigningKey.getKey());
1131
+ if (validatedToken.payload.exp <= currentTimestampSeconds()) {
1132
+ throw new InvalidTokenError('Challenge token expired');
1133
+ }
1134
+ return validatedToken;
1135
+ }
1136
+ async createTotpChallengeToken(subject, data, remember) {
1137
+ const iat = currentTimestampSeconds();
1138
+ const expiration = this.#options.totpChallengeTokenTimeToLive ?? (5 * millisecondsPerMinute);
1139
+ const exp = iat + timestampToTimestampSeconds(expiration);
1140
+ const jsonToken = {
1141
+ header: {
1142
+ alg: 'KMAC256',
1143
+ typ: 'JWT',
1144
+ },
1145
+ payload: {
1146
+ iat,
1147
+ exp,
1148
+ tenant: subject.tenantId,
1149
+ subject: subject.id,
1150
+ remember,
1151
+ data,
1152
+ },
1153
+ };
1154
+ return await createJwtTokenString(jsonToken, await this.#totpChallengeSigningKey.getKey());
1155
+ }
1156
+ async verifyAndUseRecoveryCode(totp, code) {
1157
+ const recoveryCodes = await this.#totpRecoveryCodeRepository.loadManyByQuery({ tenantId: totp.tenantId, totpId: totp.id, usedTimestamp: null });
1158
+ const hashingOptions = this.#getTotpRecoveryCodeHashingOptions(totp.recoveryCodeSalt);
1159
+ const hash = await hashTotpRecoveryCode(code, hashingOptions);
1160
+ for (const recoveryCode of recoveryCodes) {
1161
+ const isValid = await verifyTotpRecoveryCode(hash, recoveryCode.code, hashingOptions);
1162
+ if (isValid) {
1163
+ await this.#totpRecoveryCodeRepository.updateByQuery({ tenantId: recoveryCode.tenantId, id: recoveryCode.id }, { usedTimestamp: TRANSACTION_TIMESTAMP });
1164
+ return true;
1165
+ }
1166
+ }
1167
+ return false;
1168
+ }
1169
+ async verifyAndRecordTotpToken(totp, token, authAuditor) {
1170
+ const secret = await importHmacKey('raw-secret', this.totpOptions.codeHashAlgorithm, totp.secret, false);
1171
+ const isValid = await verifyTotpToken(secret, token, this.#getTotpOptions(totp.recoveryCodeSalt));
1172
+ if (!isValid) {
1173
+ return false;
1174
+ }
1175
+ const inserted = await this.#usedTotpTokenRepository.tryInsert({
1176
+ tenantId: totp.tenantId,
1177
+ subjectId: totp.subjectId,
1178
+ token,
1179
+ });
1180
+ if (isUndefined(inserted)) {
1181
+ await authAuditor.warn('totp-token-reused', {
1182
+ tenantId: totp.tenantId,
1183
+ actor: totp.subjectId,
1184
+ actorType: ActorType.Subject,
1185
+ targetId: totp.subjectId,
1186
+ targetType: 'Subject',
1187
+ details: { token },
1188
+ });
1189
+ return false;
1190
+ }
1191
+ return true;
1192
+ }
1193
+ #getTotpOptions(salt) {
1194
+ return {
1195
+ ...this.totpOptions,
1196
+ recoveryCodeHashOptions: this.#getTotpRecoveryCodeHashingOptions(salt),
1197
+ };
1198
+ }
1199
+ #getTotpRecoveryCodeHashingOptions(salt) {
1200
+ return {
1201
+ algorithm: {
1202
+ ...this.totpOptions.recoveryCodeHashOptions.algorithm,
1203
+ nonce: salt,
1204
+ },
1205
+ length: this.totpOptions.recoveryCodeHashOptions.length,
1206
+ };
785
1207
  }
786
1208
  };
787
1209
  AuthenticationService = AuthenticationService_1 = __decorate([