@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
|
@@ -19,8 +19,10 @@ describe('AuthenticationApiController Remember Functionality', () => {
|
|
|
19
19
|
const tenantId = crypto.randomUUID();
|
|
20
20
|
beforeAll(async () => {
|
|
21
21
|
({ injector, database } = await setupIntegrationTest({
|
|
22
|
-
modules: { authentication: true, audit: true, keyValueStore: true },
|
|
23
|
-
|
|
22
|
+
modules: { authentication: true, audit: true, keyValueStore: true, signals: true, api: true },
|
|
23
|
+
authentication: {
|
|
24
|
+
ancillaryService: DefaultAuthenticationAncillaryService,
|
|
25
|
+
},
|
|
24
26
|
}));
|
|
25
27
|
authenticationService = await injector.resolveAsync(AuthenticationService);
|
|
26
28
|
subjectService = await injector.resolveAsync(SubjectService);
|
|
@@ -31,13 +33,14 @@ describe('AuthenticationApiController Remember Functionality', () => {
|
|
|
31
33
|
await injector?.dispose();
|
|
32
34
|
});
|
|
33
35
|
beforeEach(async () => {
|
|
34
|
-
await clearTenantData(database, schema, ['
|
|
36
|
+
await clearTenantData(database, schema, ['used_totp_tokens', 'totp_recovery_code', 'totp', 'password', 'session', 'user', 'service_account', 'system_account', 'subject'], tenantId);
|
|
35
37
|
});
|
|
36
38
|
test('login with remember: true should have Expires in cookies', async () => {
|
|
37
39
|
const user = await subjectService.createUser({ tenantId, email: 'api-rem@example.com', firstName: 'A', lastName: 'L' });
|
|
38
|
-
await authenticationService.
|
|
40
|
+
await authenticationService.setPassword(user, 'Pass-R3m3mb3r-2026!');
|
|
39
41
|
const context = {
|
|
40
|
-
|
|
42
|
+
request: { ip: '127.0.0.1' },
|
|
43
|
+
parameters: { tenantId, subject: user.id, password: 'Pass-R3m3mb3r-2026!', remember: true, data: undefined },
|
|
41
44
|
getAuditor: async () => auditor,
|
|
42
45
|
};
|
|
43
46
|
const response = await controller.login(context);
|
|
@@ -50,9 +53,10 @@ describe('AuthenticationApiController Remember Functionality', () => {
|
|
|
50
53
|
});
|
|
51
54
|
test('login with remember: false should NOT have Expires in cookies', async () => {
|
|
52
55
|
const user = await subjectService.createUser({ tenantId, email: 'api-no-rem@example.com', firstName: 'A', lastName: 'L' });
|
|
53
|
-
await authenticationService.
|
|
56
|
+
await authenticationService.setPassword(user, 'Pass-R3m3mb3r-2026!');
|
|
54
57
|
const context = {
|
|
55
|
-
|
|
58
|
+
request: { ip: '127.0.0.1' },
|
|
59
|
+
parameters: { tenantId, subject: user.id, password: 'Pass-R3m3mb3r-2026!', remember: false, data: undefined },
|
|
56
60
|
getAuditor: async () => auditor,
|
|
57
61
|
};
|
|
58
62
|
const response = await controller.login(context);
|
|
@@ -64,18 +68,20 @@ describe('AuthenticationApiController Remember Functionality', () => {
|
|
|
64
68
|
});
|
|
65
69
|
test('refresh should propagate remember status to cookies', async () => {
|
|
66
70
|
const user = await subjectService.createUser({ tenantId, email: 'api-refresh-rem@example.com', firstName: 'A', lastName: 'L' });
|
|
67
|
-
await authenticationService.
|
|
71
|
+
await authenticationService.setPassword(user, 'Pass-R3m3mb3r-2026!');
|
|
68
72
|
// 1. Login with remember: true
|
|
69
73
|
const loginResult = await authenticationService.login({ tenantId, subject: user.id }, 'Pass-R3m3mb3r-2026!', undefined, auditor, true);
|
|
74
|
+
expect(loginResult.type).toBe('success');
|
|
70
75
|
// 2. Refresh
|
|
71
76
|
const context = {
|
|
72
77
|
request: {
|
|
78
|
+
ip: '127.0.0.1',
|
|
73
79
|
headers: new HttpHeaders({
|
|
74
|
-
'X-Refresh-Token': `Bearer ${loginResult.refreshToken}
|
|
80
|
+
'X-Refresh-Token': `Bearer ${loginResult.result.refreshToken}`,
|
|
75
81
|
}),
|
|
76
82
|
cookies: {
|
|
77
|
-
tryGet: () => undefined
|
|
78
|
-
}
|
|
83
|
+
tryGet: () => undefined,
|
|
84
|
+
},
|
|
79
85
|
},
|
|
80
86
|
parameters: { data: undefined },
|
|
81
87
|
getAuditor: async () => auditor,
|
|
@@ -87,15 +93,17 @@ describe('AuthenticationApiController Remember Functionality', () => {
|
|
|
87
93
|
}
|
|
88
94
|
// 3. Login with remember: false
|
|
89
95
|
const loginResultNoRem = await authenticationService.login({ tenantId, subject: user.id }, 'Pass-R3m3mb3r-2026!', undefined, auditor, false);
|
|
96
|
+
expect(loginResultNoRem.type).toBe('success');
|
|
90
97
|
// 4. Refresh
|
|
91
98
|
const contextNoRem = {
|
|
92
99
|
request: {
|
|
100
|
+
ip: '127.0.0.1',
|
|
93
101
|
headers: new HttpHeaders({
|
|
94
|
-
'X-Refresh-Token': `Bearer ${loginResultNoRem.refreshToken}
|
|
102
|
+
'X-Refresh-Token': `Bearer ${loginResultNoRem.result.refreshToken}`,
|
|
95
103
|
}),
|
|
96
104
|
cookies: {
|
|
97
|
-
tryGet: () => undefined
|
|
98
|
-
}
|
|
105
|
+
tryGet: () => undefined,
|
|
106
|
+
},
|
|
99
107
|
},
|
|
100
108
|
parameters: { data: undefined },
|
|
101
109
|
getAuditor: async () => auditor,
|
|
@@ -2,7 +2,6 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'vitest'
|
|
|
2
2
|
import { Auditor } from '../../audit/index.js';
|
|
3
3
|
import { clearTenantData, setupIntegrationTest } from '../../testing/index.js';
|
|
4
4
|
import { AuthenticationService } from '../server/authentication.service.js';
|
|
5
|
-
import { getRefreshTokenFromString } from '../server/helper.js';
|
|
6
5
|
import { SubjectService } from '../server/subject.service.js';
|
|
7
6
|
import { DefaultAuthenticationAncillaryService } from './authentication.test-ancillary-service.js';
|
|
8
7
|
describe('AuthenticationService Remember Functionality', () => {
|
|
@@ -16,7 +15,9 @@ describe('AuthenticationService Remember Functionality', () => {
|
|
|
16
15
|
beforeAll(async () => {
|
|
17
16
|
({ injector, database } = await setupIntegrationTest({
|
|
18
17
|
modules: { authentication: true },
|
|
19
|
-
|
|
18
|
+
authentication: {
|
|
19
|
+
ancillaryService: DefaultAuthenticationAncillaryService,
|
|
20
|
+
},
|
|
20
21
|
}));
|
|
21
22
|
authenticationService = await injector.resolveAsync(AuthenticationService);
|
|
22
23
|
subjectService = await injector.resolveAsync(SubjectService);
|
|
@@ -26,7 +27,7 @@ describe('AuthenticationService Remember Functionality', () => {
|
|
|
26
27
|
await injector?.dispose();
|
|
27
28
|
});
|
|
28
29
|
beforeEach(async () => {
|
|
29
|
-
await clearTenantData(database, schema, ['
|
|
30
|
+
await clearTenantData(database, schema, ['password', 'session', 'user', 'service_account', 'system_account', 'subject'], tenantId);
|
|
30
31
|
});
|
|
31
32
|
test('RefreshToken should contain remember flag', async () => {
|
|
32
33
|
const user = await subjectService.createUser({
|
|
@@ -36,7 +37,7 @@ describe('AuthenticationService Remember Functionality', () => {
|
|
|
36
37
|
lastName: 'Ember',
|
|
37
38
|
});
|
|
38
39
|
const tokenResult = await authenticationService.getToken(user, undefined, { remember: true });
|
|
39
|
-
const refreshToken = await
|
|
40
|
+
const refreshToken = await authenticationService.validateRefreshToken(tokenResult.refreshToken);
|
|
40
41
|
expect(refreshToken.payload).toHaveProperty('remember', true);
|
|
41
42
|
expect(tokenResult.remember).toBe(true);
|
|
42
43
|
});
|
|
@@ -44,33 +45,39 @@ describe('AuthenticationService Remember Functionality', () => {
|
|
|
44
45
|
const user = await subjectService.createUser({ tenantId, email: 'ttl@example.com', firstName: 'T', lastName: 'L' });
|
|
45
46
|
const normalResult = await authenticationService.getToken(user, undefined, { remember: false });
|
|
46
47
|
const rememberResult = await authenticationService.getToken(user, undefined, { remember: true });
|
|
47
|
-
const normalToken = await
|
|
48
|
-
const rememberToken = await
|
|
48
|
+
const normalToken = await authenticationService.validateRefreshToken(normalResult.refreshToken);
|
|
49
|
+
const rememberToken = await authenticationService.validateRefreshToken(rememberResult.refreshToken);
|
|
49
50
|
expect(rememberToken.payload.exp).toBeGreaterThan(normalToken.payload.exp);
|
|
50
51
|
});
|
|
51
52
|
test('login should pass remember flag to getToken', async () => {
|
|
52
53
|
const user = await subjectService.createUser({ tenantId, email: 'login-rem@example.com', firstName: 'L', lastName: 'R' });
|
|
53
|
-
await authenticationService.
|
|
54
|
+
await authenticationService.setPassword(user, 'Strong-Password-2026!');
|
|
54
55
|
const result = await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, auditor, true);
|
|
55
|
-
expect(result.
|
|
56
|
-
const
|
|
56
|
+
expect(result.type).toBe('success');
|
|
57
|
+
const tokenResult = result.result;
|
|
58
|
+
expect(tokenResult.remember).toBe(true);
|
|
59
|
+
const refreshToken = await authenticationService.validateRefreshToken(tokenResult.refreshToken);
|
|
57
60
|
expect(refreshToken.payload.remember).toBe(true);
|
|
58
61
|
});
|
|
59
62
|
test('refresh should propagate remember flag', async () => {
|
|
60
63
|
const user = await subjectService.createUser({ tenantId, email: 'refresh-rem@example.com', firstName: 'R', lastName: 'R' });
|
|
61
|
-
await authenticationService.
|
|
64
|
+
await authenticationService.setPassword(user, 'Strong-Password-2026!');
|
|
62
65
|
const loginResult = await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, auditor, true);
|
|
63
|
-
expect(loginResult.
|
|
64
|
-
const
|
|
66
|
+
expect(loginResult.type).toBe('success');
|
|
67
|
+
const loginSuccessResult = loginResult.result;
|
|
68
|
+
expect(loginSuccessResult.remember).toBe(true);
|
|
69
|
+
const refreshResult = await authenticationService.refresh(loginSuccessResult.refreshToken, undefined, {}, auditor);
|
|
65
70
|
expect(refreshResult.remember).toBe(true);
|
|
66
|
-
const newRefreshToken = await
|
|
71
|
+
const newRefreshToken = await authenticationService.validateRefreshToken(refreshResult.refreshToken);
|
|
67
72
|
expect(newRefreshToken.payload.remember).toBe(true);
|
|
68
73
|
// Verify it also works when not remembered
|
|
69
74
|
const loginResultNoRem = await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, auditor, false);
|
|
70
|
-
expect(loginResultNoRem.
|
|
71
|
-
const
|
|
75
|
+
expect(loginResultNoRem.type).toBe('success');
|
|
76
|
+
const loginSuccessResultNoRem = loginResultNoRem.result;
|
|
77
|
+
expect(loginSuccessResultNoRem.remember).toBe(false);
|
|
78
|
+
const refreshResultNoRem = await authenticationService.refresh(loginSuccessResultNoRem.refreshToken, undefined, {}, auditor);
|
|
72
79
|
expect(refreshResultNoRem.remember).toBe(false);
|
|
73
|
-
const newRefreshTokenNoRem = await
|
|
80
|
+
const newRefreshTokenNoRem = await authenticationService.validateRefreshToken(refreshResultNoRem.refreshToken);
|
|
74
81
|
expect(newRefreshTokenNoRem.payload.remember).toBe(false);
|
|
75
82
|
});
|
|
76
83
|
});
|
|
@@ -92,11 +92,11 @@ describe('SubjectService', () => {
|
|
|
92
92
|
test('updateUser should update user details', async () => {
|
|
93
93
|
await runInInjectionContext(injector, async () => {
|
|
94
94
|
const user = await subjectService.createUser({ tenantId, email: 'update@example.com', firstName: 'Old', lastName: 'Name' });
|
|
95
|
-
await subjectService.updateUser(tenantId, user.id, { firstName: 'New', status: SubjectStatus.
|
|
95
|
+
await subjectService.updateUser(tenantId, user.id, { firstName: 'New', status: SubjectStatus.Suspended });
|
|
96
96
|
const updated = await subjectService.getUser(tenantId, user.id);
|
|
97
97
|
expect(updated.firstName).toBe('New');
|
|
98
98
|
expect(updated.lastName).toBe('Name');
|
|
99
|
-
expect(updated.status).toBe(SubjectStatus.
|
|
99
|
+
expect(updated.status).toBe(SubjectStatus.Suspended);
|
|
100
100
|
});
|
|
101
101
|
});
|
|
102
102
|
test('updateUserEmail should update user email', async () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'vitest';
|
|
2
|
+
import { ActorType, Auditor } from '../../audit/index.js';
|
|
3
|
+
import { clearTenantData, setupIntegrationTest } from '../../testing/index.js';
|
|
4
|
+
import { SubjectStatus } from '../models/index.js';
|
|
5
|
+
import { AuthenticationAncillaryService } from '../server/authentication-ancillary.service.js';
|
|
6
|
+
import { AuthenticationService } from '../server/authentication.service.js';
|
|
7
|
+
import { SubjectService } from '../server/subject.service.js';
|
|
8
|
+
import { DefaultAuthenticationAncillaryService } from './authentication.test-ancillary-service.js';
|
|
9
|
+
describe('Suspended Subject Authentication', () => {
|
|
10
|
+
let injector;
|
|
11
|
+
let database;
|
|
12
|
+
let authenticationService;
|
|
13
|
+
let subjectService;
|
|
14
|
+
let auditor;
|
|
15
|
+
const schema = 'authentication';
|
|
16
|
+
const tenantId = crypto.randomUUID();
|
|
17
|
+
beforeAll(async () => {
|
|
18
|
+
({ injector, database } = await setupIntegrationTest({
|
|
19
|
+
modules: { authentication: true },
|
|
20
|
+
authentication: {
|
|
21
|
+
ancillaryService: DefaultAuthenticationAncillaryService,
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
authenticationService = await injector.resolveAsync(AuthenticationService);
|
|
25
|
+
subjectService = await injector.resolveAsync(SubjectService);
|
|
26
|
+
auditor = injector.resolve(Auditor);
|
|
27
|
+
});
|
|
28
|
+
afterAll(async () => {
|
|
29
|
+
await injector?.dispose();
|
|
30
|
+
});
|
|
31
|
+
beforeEach(async () => {
|
|
32
|
+
await clearTenantData(database, schema, ['totp_recovery_code', 'totp', 'password', 'session', 'user', 'service_account', 'system_account', 'subject'], tenantId);
|
|
33
|
+
});
|
|
34
|
+
test('login should FAIL for suspended user', async () => {
|
|
35
|
+
const user = await subjectService.createUser({
|
|
36
|
+
tenantId,
|
|
37
|
+
email: 'suspended@example.com',
|
|
38
|
+
firstName: 'Suspended',
|
|
39
|
+
lastName: 'User',
|
|
40
|
+
status: SubjectStatus.Suspended,
|
|
41
|
+
});
|
|
42
|
+
await authenticationService.setPassword(user, 'Strong-Password-2026!');
|
|
43
|
+
const loginPromise = authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, auditor.with({ actor: user.id, actorType: ActorType.Subject }));
|
|
44
|
+
await expect(loginPromise).rejects.toThrow('Invalid credentials.');
|
|
45
|
+
});
|
|
46
|
+
test('refresh should FAIL for suspended user', async () => {
|
|
47
|
+
const user = await subjectService.createUser({
|
|
48
|
+
tenantId,
|
|
49
|
+
email: 'suspended-refresh@example.com',
|
|
50
|
+
firstName: 'Suspended',
|
|
51
|
+
lastName: 'Refresh',
|
|
52
|
+
status: SubjectStatus.Active, // Start active to get a token
|
|
53
|
+
});
|
|
54
|
+
await authenticationService.setPassword(user, 'Strong-Password-2026!');
|
|
55
|
+
const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
|
|
56
|
+
const loginResult = await authenticationService.login({ tenantId, subject: user.id }, 'Strong-Password-2026!', undefined, userAuditor);
|
|
57
|
+
expect(loginResult.type).toBe('success');
|
|
58
|
+
// Now suspend the user
|
|
59
|
+
await subjectService.updateUser(tenantId, user.id, { status: SubjectStatus.Suspended });
|
|
60
|
+
const refreshPromise = authenticationService.refresh(loginResult.result.refreshToken, undefined, {}, userAuditor);
|
|
61
|
+
await expect(refreshPromise).rejects.toThrow('Subject is suspended.');
|
|
62
|
+
});
|
|
63
|
+
test('impersonate should FAIL if target is suspended', async () => {
|
|
64
|
+
const admin = await subjectService.createUser({ tenantId, email: 'admin@example.com', firstName: 'A', lastName: 'D' });
|
|
65
|
+
const user = await subjectService.createUser({
|
|
66
|
+
tenantId,
|
|
67
|
+
email: 'suspended-target@example.com',
|
|
68
|
+
firstName: 'S',
|
|
69
|
+
lastName: 'T',
|
|
70
|
+
status: SubjectStatus.Suspended,
|
|
71
|
+
});
|
|
72
|
+
const adminAuditor = auditor.with({ actor: admin.id, actorType: ActorType.Subject });
|
|
73
|
+
const adminToken = await authenticationService.getToken(admin, undefined);
|
|
74
|
+
const impersonatePromise = authenticationService.impersonate(adminToken.token, adminToken.refreshToken, user.id, undefined, adminAuditor);
|
|
75
|
+
await expect(impersonatePromise).rejects.toThrow('Subject is suspended.');
|
|
76
|
+
});
|
|
77
|
+
test('changePassword should FAIL for suspended user', async () => {
|
|
78
|
+
const user = await subjectService.createUser({
|
|
79
|
+
tenantId,
|
|
80
|
+
email: 'suspended-change@example.com',
|
|
81
|
+
firstName: 'S',
|
|
82
|
+
lastName: 'C',
|
|
83
|
+
status: SubjectStatus.Suspended,
|
|
84
|
+
});
|
|
85
|
+
await authenticationService.setPassword(user, 'Old-Password-2026!');
|
|
86
|
+
const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
|
|
87
|
+
const changePasswordPromise = authenticationService.changePassword({ tenantId, subject: user.id }, 'Old-Password-2026!', 'New-Password-2026!', userAuditor);
|
|
88
|
+
await expect(changePasswordPromise).rejects.toThrow('Invalid credentials.');
|
|
89
|
+
});
|
|
90
|
+
test('resetPassword should FAIL for suspended user', async () => {
|
|
91
|
+
const user = await subjectService.createUser({
|
|
92
|
+
tenantId,
|
|
93
|
+
email: 'suspended-reset@example.com',
|
|
94
|
+
firstName: 'S',
|
|
95
|
+
lastName: 'R',
|
|
96
|
+
status: SubjectStatus.Active,
|
|
97
|
+
});
|
|
98
|
+
const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
|
|
99
|
+
// @ts-ignore - access private for test or use public init
|
|
100
|
+
const passwordResetToken = await authenticationService.createPasswordResetToken(user, Date.now() + 100000);
|
|
101
|
+
// Suspend after token issue
|
|
102
|
+
await subjectService.updateUser(tenantId, user.id, { status: SubjectStatus.Suspended });
|
|
103
|
+
const resetPasswordPromise = authenticationService.resetPassword(passwordResetToken.token, 'New-Password-Reset-2026!', userAuditor);
|
|
104
|
+
await expect(resetPasswordPromise).rejects.toThrow('Subject is suspended.');
|
|
105
|
+
});
|
|
106
|
+
test('initPasswordReset should SILENTLY IGNORE suspended user', async () => {
|
|
107
|
+
const user = await subjectService.createUser({
|
|
108
|
+
tenantId,
|
|
109
|
+
email: 'suspended-init-reset@example.com',
|
|
110
|
+
firstName: 'S',
|
|
111
|
+
lastName: 'IR',
|
|
112
|
+
status: SubjectStatus.Suspended,
|
|
113
|
+
});
|
|
114
|
+
const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
|
|
115
|
+
const ancillaryService = await injector.resolveAsync(AuthenticationAncillaryService);
|
|
116
|
+
ancillaryService.lastResetData = undefined;
|
|
117
|
+
await authenticationService.initPasswordReset({ tenantId, subject: user.id }, undefined, userAuditor);
|
|
118
|
+
expect(ancillaryService.lastResetData).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'vitest';
|
|
2
|
+
import { ActorType, Auditor } from '../../audit/index.js';
|
|
3
|
+
import { importHmacKey, importKey } from '../../cryptography/index.js';
|
|
4
|
+
import { generateTotpToken } from '../../cryptography/totp.js';
|
|
5
|
+
import { clearTenantData, setupIntegrationTest } from '../../testing/index.js';
|
|
6
|
+
import { currentTimestampSeconds } from '../../utils/date-time.js';
|
|
7
|
+
import { TotpStatus } from '../models/authentication-totp.model.js';
|
|
8
|
+
import { AuthenticationService } from '../server/authentication.service.js';
|
|
9
|
+
import { SubjectService } from '../server/subject.service.js';
|
|
10
|
+
import { DefaultAuthenticationAncillaryService } from './authentication.test-ancillary-service.js';
|
|
11
|
+
describe('TOTP Enrollment', () => {
|
|
12
|
+
let injector;
|
|
13
|
+
let database;
|
|
14
|
+
let authenticationService;
|
|
15
|
+
let subjectService;
|
|
16
|
+
let auditor;
|
|
17
|
+
const schema = 'authentication';
|
|
18
|
+
const tenantId = crypto.randomUUID();
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
({ injector, database } = await setupIntegrationTest({
|
|
21
|
+
modules: { authentication: true },
|
|
22
|
+
authentication: {
|
|
23
|
+
ancillaryService: DefaultAuthenticationAncillaryService,
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
authenticationService = await injector.resolveAsync(AuthenticationService);
|
|
27
|
+
subjectService = await injector.resolveAsync(SubjectService);
|
|
28
|
+
auditor = injector.resolve(Auditor);
|
|
29
|
+
});
|
|
30
|
+
afterAll(async () => {
|
|
31
|
+
await injector?.dispose();
|
|
32
|
+
});
|
|
33
|
+
beforeEach(async () => {
|
|
34
|
+
await clearTenantData(database, schema, ['used_totp_tokens', 'totp_recovery_code', 'totp', 'password', 'session', 'user', 'service_account', 'system_account', 'subject'], tenantId);
|
|
35
|
+
});
|
|
36
|
+
test('initEnrollTotp should initiate enrollment', async () => {
|
|
37
|
+
const user = await subjectService.createUser({
|
|
38
|
+
tenantId,
|
|
39
|
+
email: 'totp-test@example.com',
|
|
40
|
+
firstName: 'Totp',
|
|
41
|
+
lastName: 'User',
|
|
42
|
+
});
|
|
43
|
+
const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
|
|
44
|
+
const result = await authenticationService.initEnrollTotp(tenantId, user.id, userAuditor);
|
|
45
|
+
expect(result.secret).toBeDefined();
|
|
46
|
+
expect(result.uri).toContain('otpauth://totp/');
|
|
47
|
+
expect(result.uri).toContain('secret=' + result.secret);
|
|
48
|
+
const totpEntry = await authenticationService.tryGetTotp(tenantId, user.id);
|
|
49
|
+
expect(totpEntry).toBeDefined();
|
|
50
|
+
expect(totpEntry.status).toBe(TotpStatus.Pending);
|
|
51
|
+
});
|
|
52
|
+
test('completeEnrollTotp should activate TOTP after verification', async () => {
|
|
53
|
+
const user = await subjectService.createUser({
|
|
54
|
+
tenantId,
|
|
55
|
+
email: 'totp-test-complete@example.com',
|
|
56
|
+
firstName: 'Totp',
|
|
57
|
+
lastName: 'Complete',
|
|
58
|
+
});
|
|
59
|
+
const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
|
|
60
|
+
const { secret } = await authenticationService.initEnrollTotp(tenantId, user.id, userAuditor);
|
|
61
|
+
// We need to convert base32 secret back to Uint8Array for generateTotpToken if it's base32
|
|
62
|
+
// But the utility probably expects Uint8Array or we use the utility's own secret generation
|
|
63
|
+
// Actually initEnrollTotp returns base32 secret for the user to enter in app.
|
|
64
|
+
// For testing, let's assume we can get the raw secret from the DB or similar.
|
|
65
|
+
const totpEntry = await authenticationService.tryGetTotp(tenantId, user.id);
|
|
66
|
+
const secretKey = await importKey('raw-secret', totpEntry.secret, { name: 'HMAC', hash: authenticationService.getTotpOptions().codeHashAlgorithm }, false, ['sign']);
|
|
67
|
+
const token = await generateTotpToken(secretKey, authenticationService.getTotpOptions());
|
|
68
|
+
const completeResult = await authenticationService.completeEnrollTotp(tenantId, user.id, token, userAuditor);
|
|
69
|
+
expect(completeResult.recoveryCodes).toHaveLength(10);
|
|
70
|
+
const activeTotpEntry = await authenticationService.tryGetTotp(tenantId, user.id);
|
|
71
|
+
expect(activeTotpEntry.status).toBe(TotpStatus.Active);
|
|
72
|
+
});
|
|
73
|
+
test('disableTotp should deactivate TOTP', async () => {
|
|
74
|
+
const user = await subjectService.createUser({
|
|
75
|
+
tenantId,
|
|
76
|
+
email: 'totp-test-disable@example.com',
|
|
77
|
+
firstName: 'Totp',
|
|
78
|
+
lastName: 'Disable',
|
|
79
|
+
});
|
|
80
|
+
const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
|
|
81
|
+
await authenticationService.initEnrollTotp(tenantId, user.id, userAuditor);
|
|
82
|
+
const totpEntry = await authenticationService.tryGetTotp(tenantId, user.id);
|
|
83
|
+
const secretKey = await importKey('raw-secret', totpEntry.secret, { name: 'HMAC', hash: authenticationService.getTotpOptions().codeHashAlgorithm }, false, ['sign']);
|
|
84
|
+
const token = await generateTotpToken(secretKey, { ...authenticationService.getTotpOptions(), timestamp: currentTimestampSeconds() - 30 });
|
|
85
|
+
await authenticationService.completeEnrollTotp(tenantId, user.id, token, userAuditor);
|
|
86
|
+
const token2 = await generateTotpToken(secretKey, authenticationService.getTotpOptions());
|
|
87
|
+
await authenticationService.disableTotp(tenantId, user.id, token2, userAuditor);
|
|
88
|
+
const disabledTotpEntry = await authenticationService.tryGetTotp(tenantId, user.id);
|
|
89
|
+
expect(disabledTotpEntry).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
test('disableTotp should fail with recovery code (decoupled)', async () => {
|
|
92
|
+
const user = await subjectService.createUser({
|
|
93
|
+
tenantId,
|
|
94
|
+
email: 'totp-test-disable-fail@example.com',
|
|
95
|
+
firstName: 'Totp',
|
|
96
|
+
lastName: 'DisableFail',
|
|
97
|
+
});
|
|
98
|
+
const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
|
|
99
|
+
await authenticationService.initEnrollTotp(tenantId, user.id, userAuditor);
|
|
100
|
+
const totpEntry = (await authenticationService.tryGetTotp(tenantId, user.id));
|
|
101
|
+
const secretKey = await importHmacKey('raw-secret', authenticationService.getTotpOptions().codeHashAlgorithm, totpEntry.secret, false);
|
|
102
|
+
const token = await generateTotpToken(secretKey, { ...authenticationService.getTotpOptions(), timestamp: currentTimestampSeconds() - 30 });
|
|
103
|
+
const { recoveryCodes } = await authenticationService.completeEnrollTotp(tenantId, user.id, token, userAuditor);
|
|
104
|
+
await expect(authenticationService.disableTotp(tenantId, user.id, recoveryCodes[0], userAuditor)).rejects.toThrow();
|
|
105
|
+
});
|
|
106
|
+
test('disableTotpWithRecoveryCode should deactivate TOTP', async () => {
|
|
107
|
+
const user = await subjectService.createUser({
|
|
108
|
+
tenantId,
|
|
109
|
+
email: 'totp-test-disable-recovery@example.com',
|
|
110
|
+
firstName: 'Totp',
|
|
111
|
+
lastName: 'DisableRecovery',
|
|
112
|
+
});
|
|
113
|
+
const userAuditor = auditor.with({ actor: user.id, actorType: ActorType.Subject });
|
|
114
|
+
await authenticationService.initEnrollTotp(tenantId, user.id, userAuditor);
|
|
115
|
+
const totpEntry = (await authenticationService.tryGetTotp(tenantId, user.id));
|
|
116
|
+
const secretKey = await importHmacKey('raw-secret', authenticationService.getTotpOptions().codeHashAlgorithm, totpEntry.secret, false);
|
|
117
|
+
const token = await generateTotpToken(secretKey, { ...authenticationService.getTotpOptions(), timestamp: currentTimestampSeconds() - 30 });
|
|
118
|
+
const { recoveryCodes } = await authenticationService.completeEnrollTotp(tenantId, user.id, token, userAuditor);
|
|
119
|
+
await authenticationService.disableTotpWithRecoveryCode(tenantId, user.id, recoveryCodes[0], userAuditor);
|
|
120
|
+
const disabledTotpEntry = await authenticationService.tryGetTotp(tenantId, user.id);
|
|
121
|
+
expect(disabledTotpEntry).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|