@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.
- package/api/response.js +4 -3
- package/api/server/gateway.js +9 -3
- package/audit/auditor.d.ts +1 -2
- package/audit/drizzle/{0000_lumpy_thunderball.sql → 0000_shallow_elektra.sql} +1 -1
- package/audit/drizzle/meta/0000_snapshot.json +2 -2
- package/audit/drizzle/meta/_journal.json +2 -2
- package/authentication/README.md +87 -42
- package/authentication/authentication.api.d.ts +392 -53
- package/authentication/authentication.api.js +133 -28
- package/authentication/client/api.client.d.ts +3 -3
- package/authentication/client/api.client.js +4 -4
- package/authentication/client/authentication.service.d.ts +93 -23
- package/authentication/client/authentication.service.js +113 -28
- package/authentication/client/http-client.middleware.d.ts +1 -1
- package/authentication/client/http-client.middleware.js +5 -4
- package/authentication/client/module.d.ts +1 -1
- package/authentication/client/module.js +2 -2
- package/authentication/errors/index.d.ts +1 -1
- package/authentication/errors/index.js +1 -1
- package/authentication/errors/password-requirements.error.d.ts +5 -0
- package/authentication/errors/{secret-requirements.error.js → password-requirements.error.js} +2 -2
- package/authentication/models/authentication-password.model.d.ts +8 -0
- package/authentication/models/{authentication-credentials.model.js → authentication-password.model.js} +11 -17
- package/authentication/models/authentication-session.model.d.ts +0 -2
- package/authentication/models/authentication-session.model.js +1 -7
- package/authentication/models/authentication-totp-recovery-code.model.d.ts +6 -0
- package/authentication/models/authentication-totp-recovery-code.model.js +34 -0
- package/authentication/models/authentication-totp.model.d.ts +19 -0
- package/authentication/models/authentication-totp.model.js +51 -0
- package/authentication/models/authentication-used-totp-token.model.d.ts +5 -0
- package/authentication/models/authentication-used-totp-token.model.js +32 -0
- package/authentication/models/index.d.ts +6 -3
- package/authentication/models/index.js +6 -3
- package/authentication/models/{init-secret-reset-data.model.d.ts → init-password-reset-data.model.d.ts} +3 -3
- package/authentication/models/{init-secret-reset-data.model.js → init-password-reset-data.model.js} +5 -5
- package/authentication/models/password-check-result.model.d.ts +3 -0
- package/authentication/models/{secret-check-result.model.js → password-check-result.model.js} +6 -6
- package/authentication/models/subject.model.d.ts +0 -6
- package/authentication/models/subject.model.js +0 -6
- package/authentication/models/token.model.d.ts +16 -2
- package/authentication/server/authentication-ancillary.service.d.ts +6 -6
- package/authentication/server/authentication-ancillary.service.js +1 -1
- package/authentication/server/authentication-password-requirements.validator.d.ts +55 -0
- package/authentication/server/{authentication-secret-requirements.validator.js → authentication-password-requirements.validator.js} +22 -22
- package/authentication/server/authentication.api-controller.d.ts +55 -27
- package/authentication/server/authentication.api-controller.js +214 -39
- package/authentication/server/authentication.audit.d.ts +42 -5
- package/authentication/server/authentication.service.d.ts +182 -93
- package/authentication/server/authentication.service.js +628 -206
- package/authentication/server/drizzle/{0000_soft_tag.sql → 0000_odd_echo.sql} +59 -13
- package/authentication/server/drizzle/meta/0000_snapshot.json +345 -32
- package/authentication/server/drizzle/meta/_journal.json +2 -2
- package/authentication/server/helper.d.ts +16 -16
- package/authentication/server/helper.js +33 -34
- package/authentication/server/index.d.ts +1 -1
- package/authentication/server/index.js +1 -1
- package/authentication/server/module.d.ts +2 -2
- package/authentication/server/module.js +4 -2
- package/authentication/server/schemas.d.ts +11 -7
- package/authentication/server/schemas.js +7 -3
- package/authentication/tests/authentication-password-requirements.validator.test.js +29 -0
- package/authentication/tests/authentication.api-controller.test.js +49 -15
- package/authentication/tests/authentication.client-error-handling.test.js +3 -2
- package/authentication/tests/authentication.client-middleware.test.js +5 -5
- package/authentication/tests/authentication.client-service-methods.test.js +28 -14
- package/authentication/tests/authentication.client-service-refresh.test.js +7 -6
- package/authentication/tests/authentication.client-service.test.js +10 -8
- package/authentication/tests/authentication.service.test.js +37 -29
- package/authentication/tests/authentication.test-ancillary-service.d.ts +1 -1
- package/authentication/tests/authentication.test-ancillary-service.js +1 -1
- package/authentication/tests/brute-force-protection.test.js +211 -0
- package/authentication/tests/helper.test.js +25 -21
- package/authentication/tests/password-requirements.error.test.js +14 -0
- package/authentication/tests/remember.api.test.js +22 -14
- package/authentication/tests/remember.service.test.js +23 -16
- package/authentication/tests/subject.service.test.js +2 -2
- package/authentication/tests/suspended-subject.test.d.ts +1 -0
- package/authentication/tests/suspended-subject.test.js +120 -0
- package/authentication/tests/totp.enrollment.test.d.ts +1 -0
- package/authentication/tests/totp.enrollment.test.js +123 -0
- package/authentication/tests/totp.login.test.d.ts +1 -0
- package/authentication/tests/totp.login.test.js +213 -0
- package/authentication/tests/totp.recovery-codes.test.d.ts +1 -0
- package/authentication/tests/totp.recovery-codes.test.js +97 -0
- package/authentication/tests/totp.status.test.d.ts +1 -0
- package/authentication/tests/totp.status.test.js +72 -0
- package/circuit-breaker/postgres/drizzle/{0000_cooing_korath.sql → 0000_same_captain_cross.sql} +1 -1
- package/circuit-breaker/postgres/drizzle/meta/0000_snapshot.json +2 -2
- package/circuit-breaker/postgres/drizzle/meta/_journal.json +2 -2
- package/cryptography/cryptography.d.ts +336 -0
- package/cryptography/cryptography.js +328 -0
- package/cryptography/index.d.ts +4 -0
- package/cryptography/index.js +4 -0
- package/{utils → cryptography}/jwt.d.ts +22 -4
- package/{utils → cryptography}/jwt.js +36 -18
- package/cryptography/module.d.ts +35 -0
- package/cryptography/module.js +148 -0
- package/cryptography/tests/cryptography.test.d.ts +1 -0
- package/cryptography/tests/cryptography.test.js +175 -0
- package/cryptography/tests/jwt.test.d.ts +1 -0
- package/cryptography/tests/jwt.test.js +54 -0
- package/cryptography/tests/modern.test.d.ts +1 -0
- package/cryptography/tests/modern.test.js +105 -0
- package/cryptography/tests/module.test.d.ts +1 -0
- package/cryptography/tests/module.test.js +100 -0
- package/cryptography/tests/totp.test.d.ts +1 -0
- package/cryptography/tests/totp.test.js +108 -0
- package/cryptography/totp.d.ts +96 -0
- package/cryptography/totp.js +123 -0
- package/document-management/server/drizzle/{0000_curious_nighthawk.sql → 0000_sharp_scream.sql} +21 -21
- package/document-management/server/drizzle/meta/0000_snapshot.json +22 -22
- package/document-management/server/drizzle/meta/_journal.json +2 -2
- package/document-management/server/services/document-file.service.js +1 -1
- package/errors/errors.localization.d.ts +2 -2
- package/errors/errors.localization.js +2 -2
- package/errors/index.d.ts +1 -0
- package/errors/index.js +1 -0
- package/errors/too-many-requests.error.d.ts +5 -0
- package/errors/too-many-requests.error.js +7 -0
- package/examples/api/authentication.js +5 -5
- package/examples/api/custom-authentication.js +4 -3
- package/file/server/mime-type.js +1 -1
- package/http/http-body.d.ts +1 -0
- package/http/http-body.js +3 -0
- package/image-service/imgproxy/imgproxy-image-service.d.ts +0 -1
- package/image-service/imgproxy/imgproxy-image-service.js +9 -27
- package/key-value-store/postgres/drizzle/{0000_shocking_slipstream.sql → 0000_moaning_calypso.sql} +1 -1
- package/key-value-store/postgres/drizzle/meta/0000_snapshot.json +2 -2
- package/key-value-store/postgres/drizzle/meta/_journal.json +2 -2
- package/lock/postgres/drizzle/{0000_busy_tattoo.sql → 0000_nappy_wraith.sql} +1 -1
- package/lock/postgres/drizzle/meta/0000_snapshot.json +2 -2
- package/lock/postgres/drizzle/meta/_journal.json +2 -2
- package/logger/formatters/json.js +1 -1
- package/logger/formatters/pretty-print.js +1 -1
- package/mail/drizzle/{0000_numerous_the_watchers.sql → 0000_cultured_quicksilver.sql} +2 -2
- package/mail/drizzle/meta/0000_snapshot.json +4 -4
- package/mail/drizzle/meta/_journal.json +2 -9
- package/notification/server/drizzle/{0000_wise_pyro.sql → 0000_new_tenebrous.sql} +6 -6
- package/notification/server/drizzle/meta/0000_snapshot.json +7 -7
- package/notification/server/drizzle/meta/_journal.json +2 -2
- package/notification/tests/notification-flow.test.js +1 -8
- package/notification/tests/notification-type.service.test.js +3 -3
- package/openid-connect/oidc.service.js +2 -3
- package/orm/data-types/common.js +1 -1
- package/orm/server/drizzle/schema-converter.js +9 -4
- package/orm/server/encryption.js +1 -1
- package/orm/server/module.d.ts +0 -1
- package/orm/server/module.js +0 -4
- package/orm/server/repository.d.ts +2 -1
- package/orm/server/repository.js +7 -10
- package/orm/tests/encryption.test.js +4 -6
- package/orm/tests/repository-extra-coverage.test.js +0 -2
- package/orm/tests/repository-regression.test.js +0 -3
- package/package.json +9 -8
- package/password/README.md +1 -1
- package/password/have-i-been-pwned.js +1 -1
- package/rate-limit/postgres/drizzle/{0000_watery_rage.sql → 0000_serious_sauron.sql} +1 -1
- package/rate-limit/postgres/drizzle/meta/0000_snapshot.json +2 -2
- package/rate-limit/postgres/drizzle/meta/_journal.json +2 -2
- package/rate-limit/postgres/postgres-rate-limiter.d.ts +1 -1
- package/rate-limit/postgres/postgres-rate-limiter.js +1 -1
- package/rate-limit/rate-limiter.d.ts +1 -1
- package/rpc/tests/rpc.integration.test.js +25 -31
- package/supports.d.ts +1 -0
- package/supports.js +1 -0
- package/task-queue/postgres/drizzle/{0000_faithful_daimon_hellstrom.sql → 0000_dark_ronan.sql} +5 -5
- package/task-queue/postgres/drizzle/meta/0000_snapshot.json +10 -10
- package/task-queue/postgres/drizzle/meta/_journal.json +2 -9
- package/task-queue/postgres/task-queue.js +2 -2
- package/task-queue/tests/coverage-enhancement.test.js +2 -2
- package/test/drizzle/{0000_natural_cannonball.sql → 0000_organic_gamora.sql} +2 -2
- package/test/drizzle/meta/0000_snapshot.json +3 -4
- package/test/drizzle/meta/_journal.json +2 -9
- package/testing/integration-setup.d.ts +7 -3
- package/testing/integration-setup.js +119 -96
- package/utils/alphabet.d.ts +1 -0
- package/utils/alphabet.js +1 -0
- package/utils/base32.d.ts +4 -0
- package/utils/base32.js +49 -0
- package/utils/base64.d.ts +0 -2
- package/utils/base64.js +6 -70
- package/utils/equals.d.ts +13 -3
- package/utils/equals.js +29 -9
- package/utils/index.d.ts +1 -2
- package/utils/index.js +1 -2
- package/utils/random.d.ts +1 -0
- package/utils/random.js +14 -8
- package/authentication/errors/secret-requirements.error.d.ts +0 -5
- package/authentication/models/authentication-credentials.model.d.ts +0 -10
- package/authentication/models/secret-check-result.model.d.ts +0 -3
- package/authentication/server/authentication-secret-requirements.validator.d.ts +0 -55
- package/authentication/tests/authentication-ancillary.service.test.js +0 -13
- package/authentication/tests/authentication-secret-requirements.validator.test.js +0 -29
- package/authentication/tests/secret-requirements.error.test.js +0 -14
- package/mail/drizzle/0001_married_tarantula.sql +0 -12
- package/mail/drizzle/meta/0001_snapshot.json +0 -69
- package/orm/server/tokens.d.ts +0 -1
- package/orm/server/tokens.js +0 -2
- package/task-queue/postgres/drizzle/0001_rapid_infant_terrible.sql +0 -16
- package/task-queue/postgres/drizzle/meta/0001_snapshot.json +0 -753
- package/test/drizzle/0001_closed_the_captain.sql +0 -2
- package/test/drizzle/meta/0001_snapshot.json +0 -117
- package/utils/cryptography.d.ts +0 -137
- package/utils/cryptography.js +0 -201
- /package/authentication/tests/{authentication-ancillary.service.test.d.ts → authentication-password-requirements.validator.test.d.ts} +0 -0
- /package/authentication/tests/{authentication-secret-requirements.validator.test.d.ts → brute-force-protection.test.d.ts} +0 -0
- /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 {
|
|
12
|
-
import { InvalidCredentialsError } from '../../errors/index.js';
|
|
13
|
-
import {
|
|
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 {
|
|
24
|
-
import {
|
|
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 {
|
|
24
|
+
import { isDefined, isString, isUndefined } from '../../utils/type-guards.js';
|
|
31
25
|
import { millisecondsPerDay, millisecondsPerHour, millisecondsPerMinute } from '../../utils/units.js';
|
|
32
|
-
import {
|
|
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 {
|
|
35
|
-
import {
|
|
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
|
|
57
|
+
* How long a password reset token is valid in milliseconds.
|
|
69
58
|
*
|
|
70
59
|
* @default 10 minutes
|
|
71
60
|
*/
|
|
72
|
-
|
|
61
|
+
passwordResetTokenTimeToLive;
|
|
73
62
|
/**
|
|
74
|
-
*
|
|
63
|
+
* How long a TOTP challenge token is valid in milliseconds.
|
|
75
64
|
*
|
|
76
|
-
* @default
|
|
65
|
+
* @default 5 minutes
|
|
77
66
|
*/
|
|
78
|
-
|
|
67
|
+
totpChallengeTokenTimeToLive;
|
|
79
68
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* @default 500000
|
|
69
|
+
* Options for brute force protection.
|
|
83
70
|
*/
|
|
84
|
-
|
|
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
|
|
93
|
-
|
|
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
|
|
108
|
+
* @template AdditionalInitPasswordResetData Type of additional password reset data
|
|
110
109
|
*/
|
|
111
110
|
let AuthenticationService = AuthenticationService_1 = class AuthenticationService {
|
|
112
|
-
#
|
|
111
|
+
#passwordRepository = injectRepository(AuthenticationPassword);
|
|
113
112
|
#sessionRepository = injectRepository(AuthenticationSession);
|
|
114
113
|
#subjectRepository = injectRepository(Subject);
|
|
115
114
|
#userRepository = injectRepository(User);
|
|
116
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
157
|
-
* This method should not be exposed to the public API without an authenticated current password or
|
|
158
|
-
* @param subject The subject to set the
|
|
159
|
-
* @param
|
|
160
|
-
* @param options Options for setting the
|
|
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
|
|
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
|
|
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.#
|
|
157
|
+
await this.#authenticationPasswordRequirementsValidator.validatePasswordRequirements(password);
|
|
166
158
|
}
|
|
167
159
|
const salt = getRandomBytes(SALT_LENGTH);
|
|
168
|
-
const hash = await this.getHash(
|
|
169
|
-
await this.#
|
|
170
|
-
await this.#
|
|
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
|
|
174
|
+
* Authenticates a subject with a password.
|
|
184
175
|
* @param subject The subject to authenticate.
|
|
185
|
-
* @param
|
|
176
|
+
* @param password The password to authenticate with.
|
|
186
177
|
* @returns The result of the authentication.
|
|
187
178
|
*/
|
|
188
|
-
async
|
|
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
|
|
184
|
+
const loadedPassword = await this.#passwordRepository.tryLoadByQuery({
|
|
194
185
|
tenantId: queryTenantId,
|
|
195
186
|
subjectId: querySubjectId,
|
|
196
187
|
});
|
|
197
|
-
const
|
|
198
|
-
const hash = await this.getHash(
|
|
199
|
-
const valid = timingSafeBinaryEquals(hash,
|
|
200
|
-
|
|
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
|
|
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,
|
|
247
|
+
async login(subjectInput, password, data, auditor, remember = false) {
|
|
248
248
|
const authAuditor = auditor.fork(AuthenticationService_1.name);
|
|
249
|
-
const authenticationResult = await this.
|
|
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: '
|
|
256
|
+
targetType: 'Subject',
|
|
257
257
|
details: { subjectInput, resolvedSubjectId: actualSubject?.id ?? null },
|
|
258
258
|
});
|
|
259
259
|
throw new InvalidCredentialsError();
|
|
260
260
|
}
|
|
261
|
-
await this.
|
|
262
|
-
|
|
263
|
-
|
|
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:
|
|
267
|
-
actor:
|
|
278
|
+
tenantId: subject.tenantId,
|
|
279
|
+
actor: subject.id,
|
|
268
280
|
actorType: ActorType.Subject,
|
|
269
|
-
targetId:
|
|
270
|
-
targetType: '
|
|
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: '
|
|
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: '
|
|
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
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
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: '
|
|
538
|
+
targetType: 'Subject',
|
|
471
539
|
});
|
|
472
540
|
return tokenResult;
|
|
473
541
|
}
|
|
474
542
|
/**
|
|
475
|
-
* Initializes a
|
|
476
|
-
* @param subject The subject to reset the
|
|
477
|
-
* @param data Additional data for the
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
497
|
-
|
|
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:
|
|
574
|
+
token: passwordResetToken.token,
|
|
500
575
|
...data,
|
|
501
576
|
};
|
|
502
|
-
await this.#authenticationAncillaryService.
|
|
503
|
-
await authAuditor.info('init-
|
|
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: '
|
|
581
|
+
targetType: 'Subject',
|
|
507
582
|
});
|
|
508
583
|
}
|
|
509
584
|
/**
|
|
510
|
-
* Changes a subject's
|
|
511
|
-
* @param subjectInput The subject to change the
|
|
512
|
-
* @param
|
|
513
|
-
* @param
|
|
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
|
|
591
|
+
async changePassword(subjectInput, currentPassword, newPassword, auditor) {
|
|
517
592
|
const authAuditor = auditor.fork(AuthenticationService_1.name);
|
|
518
|
-
|
|
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-
|
|
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: '
|
|
603
|
+
targetType: 'Subject',
|
|
526
604
|
details: { subjectInput, resolvedSubjectId: resolvedSubject?.id ?? null },
|
|
527
605
|
});
|
|
528
606
|
throw new InvalidCredentialsError();
|
|
529
607
|
}
|
|
530
|
-
await this.hooks.
|
|
531
|
-
await this.
|
|
532
|
-
await this.hooks.
|
|
533
|
-
await authAuditor.info('change-
|
|
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: '
|
|
616
|
+
targetType: 'Subject',
|
|
539
617
|
});
|
|
540
618
|
}
|
|
541
619
|
/**
|
|
542
|
-
* Resets a
|
|
543
|
-
* @param tokenString The
|
|
544
|
-
* @param
|
|
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
|
|
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.
|
|
552
|
-
const
|
|
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(
|
|
557
|
-
const lastUpdateSeconds = timestampToTimestampSeconds(
|
|
637
|
+
if (isDefined(passwordRecord)) {
|
|
638
|
+
const lastUpdateSeconds = timestampToTimestampSeconds(passwordRecord.metadata.revisionTimestamp);
|
|
558
639
|
if (token.payload.iat < lastUpdateSeconds) {
|
|
559
|
-
await authAuditor.info('reset-
|
|
640
|
+
await authAuditor.info('reset-password-failure', {
|
|
560
641
|
targetId: token.payload.subject,
|
|
561
|
-
targetType: '
|
|
562
|
-
details: { reason: 'Token is invalid (
|
|
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
|
-
|
|
569
|
-
await
|
|
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: '
|
|
654
|
+
targetType: 'Subject',
|
|
573
655
|
});
|
|
574
656
|
}
|
|
575
657
|
catch (error) {
|
|
576
|
-
await authAuditor.warn('reset-
|
|
658
|
+
await authAuditor.warn('reset-password-failure', {
|
|
577
659
|
targetId: NIL_UUID,
|
|
578
|
-
targetType: '
|
|
660
|
+
targetType: 'Subject',
|
|
579
661
|
details: { reason: error.message },
|
|
580
662
|
});
|
|
581
663
|
throw error;
|
|
582
664
|
}
|
|
583
665
|
}
|
|
584
666
|
/**
|
|
585
|
-
* Checks a
|
|
586
|
-
* @param
|
|
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
|
|
590
|
-
return await this.#
|
|
671
|
+
async checkPassword(password) {
|
|
672
|
+
return await this.#authenticationPasswordRequirementsValidator.checkPasswordRequirements(password);
|
|
591
673
|
}
|
|
592
674
|
/**
|
|
593
|
-
* Tests a
|
|
594
|
-
* @param
|
|
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
|
|
598
|
-
return await this.#
|
|
679
|
+
async testPassword(password) {
|
|
680
|
+
return await this.#authenticationPasswordRequirementsValidator.testPasswordRequirements(password);
|
|
599
681
|
}
|
|
600
682
|
/**
|
|
601
|
-
* Validates a
|
|
602
|
-
* @param
|
|
603
|
-
* @throws {
|
|
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
|
|
606
|
-
await this.#
|
|
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.
|
|
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.
|
|
706
|
+
return await getRefreshTokenFromString(token, await this.#refreshTokenSigningKey.getKey());
|
|
625
707
|
}
|
|
626
708
|
/**
|
|
627
|
-
* Validates a
|
|
628
|
-
* @param token The
|
|
629
|
-
* @returns The validated
|
|
630
|
-
* @throws {InvalidTokenError} If the
|
|
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
|
|
633
|
-
return await
|
|
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: '
|
|
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.
|
|
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: '
|
|
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.
|
|
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
|
|
755
|
-
const iat =
|
|
836
|
+
async createPasswordResetToken(subject, expirationTimestamp) {
|
|
837
|
+
const iat = currentTimestampSeconds();
|
|
756
838
|
const jsonToken = {
|
|
757
839
|
header: {
|
|
758
|
-
alg: '
|
|
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.
|
|
850
|
+
const token = await createJwtTokenString(jsonToken, await this.#passwordResetTokenSigningKey.getKey());
|
|
769
851
|
return { token, jsonToken };
|
|
770
852
|
}
|
|
771
|
-
async
|
|
772
|
-
const
|
|
773
|
-
const
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
this.
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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([
|