@tstdl/base 0.93.187 → 0.93.189
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/ai/genkit/multi-region.plugin.js +15 -11
- package/ai/genkit/types.d.ts +1 -1
- package/ai/genkit/types.js +1 -1
- package/authentication/client/authentication.service.d.ts +5 -0
- package/authentication/client/authentication.service.js +21 -4
- package/authentication/models/authentication-totp-recovery-code.model.js +3 -2
- package/authentication/models/totp-results.model.d.ts +1 -0
- package/authentication/models/totp-results.model.js +6 -1
- package/authentication/server/authentication.api-controller.d.ts +1 -0
- package/authentication/server/authentication.api-controller.js +9 -2
- package/authentication/server/authentication.service.js +114 -92
- package/authentication/server/drizzle/{0000_odd_echo.sql → 0000_dry_stepford_cuckoos.sql} +2 -1
- package/authentication/server/drizzle/meta/0000_snapshot.json +24 -2
- package/authentication/server/drizzle/meta/_journal.json +2 -2
- package/circuit-breaker/circuit-breaker.d.ts +22 -10
- package/circuit-breaker/postgres/circuit-breaker.d.ts +5 -4
- package/circuit-breaker/postgres/circuit-breaker.js +21 -19
- package/circuit-breaker/postgres/drizzle/{0000_same_captain_cross.sql → 0000_dapper_hercules.sql} +3 -2
- package/circuit-breaker/postgres/drizzle/meta/0000_snapshot.json +13 -6
- package/circuit-breaker/postgres/drizzle/meta/_journal.json +2 -2
- package/circuit-breaker/postgres/model.d.ts +2 -1
- package/circuit-breaker/postgres/model.js +9 -4
- package/circuit-breaker/postgres/provider.d.ts +1 -1
- package/circuit-breaker/postgres/provider.js +2 -2
- package/circuit-breaker/provider.d.ts +6 -1
- package/cryptography/totp.d.ts +2 -1
- package/cryptography/totp.js +15 -7
- package/orm/sqls/sqls.d.ts +5 -3
- package/orm/sqls/sqls.js +5 -3
- package/package.json +4 -4
- package/rate-limit/postgres/drizzle/{0000_serious_sauron.sql → 0000_previous_zeigeist.sql} +2 -1
- package/rate-limit/postgres/drizzle/meta/0000_snapshot.json +10 -3
- package/rate-limit/postgres/drizzle/meta/_journal.json +2 -2
- package/rate-limit/postgres/postgres-rate-limiter.d.ts +1 -0
- package/rate-limit/postgres/postgres-rate-limiter.js +3 -2
- package/rate-limit/postgres/rate-limit.model.d.ts +1 -0
- package/rate-limit/postgres/rate-limit.model.js +6 -1
- package/rate-limit/postgres/rate-limiter.provider.d.ts +1 -1
- package/rate-limit/postgres/rate-limiter.provider.js +2 -2
- package/rate-limit/provider.d.ts +3 -3
- package/rate-limit/rate-limiter.d.ts +2 -1
- package/signals/notifier.js +1 -1
- package/signals/operators/derive-async.js +11 -6
- package/task-queue/postgres/task-queue.js +8 -8
- package/testing/integration-setup.js +4 -0
|
@@ -14,16 +14,20 @@ export function vertexAiMultiLocation(options) {
|
|
|
14
14
|
}
|
|
15
15
|
const tokenLimitThreshold = options.tokenLimitThreshold ?? defaultTokenLimitThreshold;
|
|
16
16
|
const locationConfigs = options.locations.map((location) => {
|
|
17
|
-
const
|
|
18
|
-
const
|
|
17
|
+
const circuitBreakerNamespace = `genkit:vertex-ai:location`;
|
|
18
|
+
const tokenLimitCircuitBreakerNamespace = `genkit:vertex-ai:location-token-limit`;
|
|
19
|
+
const resource = location;
|
|
20
|
+
const tokenLimitResource = location;
|
|
19
21
|
return {
|
|
20
22
|
location,
|
|
21
|
-
|
|
23
|
+
resource,
|
|
24
|
+
tokenLimitResource,
|
|
25
|
+
circuitBreaker: options.circuitBreakerProvider.provide(circuitBreakerNamespace, {
|
|
22
26
|
threshold: 1,
|
|
23
27
|
resetTimeout: 30 * millisecondsPerSecond,
|
|
24
28
|
...options.circuitBreakerConfig,
|
|
25
29
|
}),
|
|
26
|
-
tokenLimitCircuitBreaker: options.circuitBreakerProvider.provide(
|
|
30
|
+
tokenLimitCircuitBreaker: options.circuitBreakerProvider.provide(tokenLimitCircuitBreakerNamespace, {
|
|
27
31
|
threshold: 1,
|
|
28
32
|
resetTimeout: 15 * millisecondsPerMinute,
|
|
29
33
|
...options.tokenLimitCircuitBreakerConfig,
|
|
@@ -48,15 +52,15 @@ export function vertexAiMultiLocation(options) {
|
|
|
48
52
|
let lastError;
|
|
49
53
|
let isLargeRequest = false;
|
|
50
54
|
const skippedLocations = [];
|
|
51
|
-
for (const { location, circuitBreaker, tokenLimitCircuitBreaker } of shuffledConfigs) {
|
|
52
|
-
const check = await circuitBreaker.check();
|
|
55
|
+
for (const { location, resource, tokenLimitResource, circuitBreaker, tokenLimitCircuitBreaker } of shuffledConfigs) {
|
|
56
|
+
const check = await circuitBreaker.check(resource);
|
|
53
57
|
if (!check.allowed) {
|
|
54
58
|
options.logger.warn(`Location ${location} is currently unhealthy. Skipping...`);
|
|
55
59
|
skippedLocations.push({ location, reason: 'unhealthy' });
|
|
56
60
|
continue;
|
|
57
61
|
}
|
|
58
62
|
if (isLargeRequest) {
|
|
59
|
-
const tokenCheck = await tokenLimitCircuitBreaker.check();
|
|
63
|
+
const tokenCheck = await tokenLimitCircuitBreaker.check(tokenLimitResource);
|
|
60
64
|
if (!tokenCheck.allowed) {
|
|
61
65
|
options.logger.warn(`Location ${location} is known to have a low token limit. Skipping for this large request...`);
|
|
62
66
|
skippedLocations.push({ location, reason: 'known to have low token limits' });
|
|
@@ -73,10 +77,10 @@ export function vertexAiMultiLocation(options) {
|
|
|
73
77
|
}, {
|
|
74
78
|
onChunk: streamingCallback,
|
|
75
79
|
});
|
|
76
|
-
await circuitBreaker.recordSuccess();
|
|
80
|
+
await circuitBreaker.recordSuccess(resource);
|
|
77
81
|
const isLargeSuccess = isLargeRequest || ((result.usage?.inputTokens ?? 0) > tokenLimitThreshold);
|
|
78
82
|
if (isLargeSuccess) {
|
|
79
|
-
await tokenLimitCircuitBreaker.recordSuccess();
|
|
83
|
+
await tokenLimitCircuitBreaker.recordSuccess(tokenLimitResource);
|
|
80
84
|
}
|
|
81
85
|
return result;
|
|
82
86
|
}
|
|
@@ -93,11 +97,11 @@ export function vertexAiMultiLocation(options) {
|
|
|
93
97
|
if (isTokenLimitError) {
|
|
94
98
|
options.logger.warn(`Location ${location} responded with token limit error. Trying next location...`);
|
|
95
99
|
isLargeRequest = true;
|
|
96
|
-
await tokenLimitCircuitBreaker.recordFailure();
|
|
100
|
+
await tokenLimitCircuitBreaker.recordFailure(tokenLimitResource);
|
|
97
101
|
}
|
|
98
102
|
else {
|
|
99
103
|
options.logger.warn(`Location ${location} responded with ${error.status}. Tripping circuit breaker and trying next location...`);
|
|
100
|
-
await circuitBreaker.recordFailure();
|
|
104
|
+
await circuitBreaker.recordFailure(resource);
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
107
|
}
|
package/ai/genkit/types.d.ts
CHANGED
|
@@ -430,7 +430,7 @@ export declare abstract class VertexAiMultiLocationOptions {
|
|
|
430
430
|
/**
|
|
431
431
|
* Threshold of input tokens after which a request is considered large.
|
|
432
432
|
* A successful request with more tokens than this threshold will record a success for the token limit circuit breaker.
|
|
433
|
-
* Defaults to
|
|
433
|
+
* Defaults to 131_072.
|
|
434
434
|
*/
|
|
435
435
|
tokenLimitThreshold?: number;
|
|
436
436
|
}
|
package/ai/genkit/types.js
CHANGED
|
@@ -16,7 +16,7 @@ export class VertexAiMultiLocationOptions {
|
|
|
16
16
|
/**
|
|
17
17
|
* Threshold of input tokens after which a request is considered large.
|
|
18
18
|
* A successful request with more tokens than this threshold will record a success for the token limit circuit breaker.
|
|
19
|
-
* Defaults to
|
|
19
|
+
* Defaults to 131_072.
|
|
20
20
|
*/
|
|
21
21
|
tokenLimitThreshold;
|
|
22
22
|
}
|
|
@@ -27,6 +27,7 @@ export declare class AuthenticationClientService<AdditionalTokenPayload extends
|
|
|
27
27
|
private readonly logger;
|
|
28
28
|
private readonly disposeSignal;
|
|
29
29
|
private readonly forceRefreshRequested;
|
|
30
|
+
private readonly totpStatusReloadNotifier;
|
|
30
31
|
private readonly forceRefreshSubject;
|
|
31
32
|
private clockOffset;
|
|
32
33
|
private initialized;
|
|
@@ -193,6 +194,10 @@ export declare class AuthenticationClientService<AdditionalTokenPayload extends
|
|
|
193
194
|
* @param newPassword The new password
|
|
194
195
|
*/
|
|
195
196
|
resetPassword(token: string, newPassword: string): Promise<void>;
|
|
197
|
+
/**
|
|
198
|
+
* Reload TOTP status.
|
|
199
|
+
*/
|
|
200
|
+
reloadTotpStatus(): void;
|
|
196
201
|
/**
|
|
197
202
|
* Initiate TOTP enrollment.
|
|
198
203
|
* @returns The secret and URI for enrollment.
|
|
@@ -22,7 +22,7 @@ import { Lock } from '../../lock/index.js';
|
|
|
22
22
|
import { Logger } from '../../logger/index.js';
|
|
23
23
|
import { MessageBus } from '../../message-bus/index.js';
|
|
24
24
|
import { computed, signal, toObservable } from '../../signals/api.js';
|
|
25
|
-
import { deriveAsync } from '../../signals/index.js';
|
|
25
|
+
import { createNotifier, deriveAsync } from '../../signals/index.js';
|
|
26
26
|
import { currentTimestampSeconds } from '../../utils/date-time.js';
|
|
27
27
|
import { clamp } from '../../utils/math.js';
|
|
28
28
|
import { timeout } from '../../utils/timing.js';
|
|
@@ -74,6 +74,7 @@ let AuthenticationClientService = class AuthenticationClientService {
|
|
|
74
74
|
logger = inject(Logger, 'AuthenticationService');
|
|
75
75
|
disposeSignal = inject(CancellationSignal).fork();
|
|
76
76
|
forceRefreshRequested = signal(false);
|
|
77
|
+
totpStatusReloadNotifier = createNotifier();
|
|
77
78
|
forceRefreshSubject = new Subject();
|
|
78
79
|
clockOffset = 0;
|
|
79
80
|
initialized = false;
|
|
@@ -107,6 +108,7 @@ let AuthenticationClientService = class AuthenticationClientService {
|
|
|
107
108
|
/** Whether the user is impersonated */
|
|
108
109
|
impersonated = computed(() => isDefined(this.impersonator()));
|
|
109
110
|
totpStatus = deriveAsync(async () => {
|
|
111
|
+
this.totpStatusReloadNotifier.listen();
|
|
110
112
|
const token = this.token();
|
|
111
113
|
if (isUndefined(token)) {
|
|
112
114
|
return undefined;
|
|
@@ -403,6 +405,12 @@ let AuthenticationClientService = class AuthenticationClientService {
|
|
|
403
405
|
async resetPassword(token, newPassword) {
|
|
404
406
|
await this.client.resetPassword({ token, newPassword });
|
|
405
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Reload TOTP status.
|
|
410
|
+
*/
|
|
411
|
+
reloadTotpStatus() {
|
|
412
|
+
this.totpStatusReloadNotifier.notify();
|
|
413
|
+
}
|
|
406
414
|
/**
|
|
407
415
|
* Initiate TOTP enrollment.
|
|
408
416
|
* @returns The secret and URI for enrollment.
|
|
@@ -416,7 +424,9 @@ let AuthenticationClientService = class AuthenticationClientService {
|
|
|
416
424
|
* @returns The recovery codes.
|
|
417
425
|
*/
|
|
418
426
|
async completeEnrollTotp(token) {
|
|
419
|
-
|
|
427
|
+
const result = await this.client.completeEnrollTotp({ token });
|
|
428
|
+
this.reloadTotpStatus();
|
|
429
|
+
return result;
|
|
420
430
|
}
|
|
421
431
|
/**
|
|
422
432
|
* Disable TOTP.
|
|
@@ -424,6 +434,7 @@ let AuthenticationClientService = class AuthenticationClientService {
|
|
|
424
434
|
*/
|
|
425
435
|
async disableTotp(token) {
|
|
426
436
|
await this.client.disableTotp({ token });
|
|
437
|
+
this.reloadTotpStatus();
|
|
427
438
|
}
|
|
428
439
|
/**
|
|
429
440
|
* Disable TOTP using a recovery code.
|
|
@@ -431,6 +442,7 @@ let AuthenticationClientService = class AuthenticationClientService {
|
|
|
431
442
|
*/
|
|
432
443
|
async disableTotpWithRecoveryCode(recoveryCode) {
|
|
433
444
|
await this.client.disableTotpWithRecoveryCode({ recoveryCode });
|
|
445
|
+
this.reloadTotpStatus();
|
|
434
446
|
}
|
|
435
447
|
/**
|
|
436
448
|
* Regenerate recovery codes.
|
|
@@ -439,7 +451,9 @@ let AuthenticationClientService = class AuthenticationClientService {
|
|
|
439
451
|
* @returns The new recovery codes.
|
|
440
452
|
*/
|
|
441
453
|
async regenerateRecoveryCodes(token, options) {
|
|
442
|
-
|
|
454
|
+
const result = await this.client.regenerateRecoveryCodes({ token, ...options });
|
|
455
|
+
this.reloadTotpStatus();
|
|
456
|
+
return result;
|
|
443
457
|
}
|
|
444
458
|
/**
|
|
445
459
|
* Get TOTP activation status.
|
|
@@ -514,7 +528,10 @@ let AuthenticationClientService = class AuthenticationClientService {
|
|
|
514
528
|
const token = this.token();
|
|
515
529
|
// 1. Wait for login/token if none is present
|
|
516
530
|
if (isUndefined(token)) {
|
|
517
|
-
await firstValueFrom(race([
|
|
531
|
+
await firstValueFrom(race([
|
|
532
|
+
this.tokenUpdateBus.allMessages$.pipe(filter(isDefined)),
|
|
533
|
+
from(this.disposeSignal),
|
|
534
|
+
]), { defaultValue: undefined });
|
|
518
535
|
continue;
|
|
519
536
|
}
|
|
520
537
|
try {
|
|
@@ -7,7 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
8
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
9
|
};
|
|
10
|
-
import { Table, TenantEntity, TenantReference, TimestampProperty, UuidProperty } from '../../orm/index.js';
|
|
10
|
+
import { Index, Table, TenantEntity, TenantReference, TimestampProperty, UuidProperty } from '../../orm/index.js';
|
|
11
11
|
import { Uint8ArrayProperty } from '../../schema/index.js';
|
|
12
12
|
import { AuthenticationTotp } from './authentication-totp.model.js';
|
|
13
13
|
let AuthenticationTotpRecoveryCode = class AuthenticationTotpRecoveryCode extends TenantEntity {
|
|
@@ -29,6 +29,7 @@ __decorate([
|
|
|
29
29
|
__metadata("design:type", Object)
|
|
30
30
|
], AuthenticationTotpRecoveryCode.prototype, "usedTimestamp", void 0);
|
|
31
31
|
AuthenticationTotpRecoveryCode = __decorate([
|
|
32
|
-
Table('totp_recovery_code', { schema: 'authentication' })
|
|
32
|
+
Table('totp_recovery_code', { schema: 'authentication' }),
|
|
33
|
+
Index(['tenantId', 'totpId'])
|
|
33
34
|
], AuthenticationTotpRecoveryCode);
|
|
34
35
|
export { AuthenticationTotpRecoveryCode };
|
|
@@ -8,7 +8,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
8
8
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
|
-
import { Array, BooleanProperty, StringProperty } from '../../schema/index.js';
|
|
11
|
+
import { Array, BooleanProperty, NumberProperty, StringProperty } from '../../schema/index.js';
|
|
12
12
|
export class TotpEnrollmentInitResult {
|
|
13
13
|
secret;
|
|
14
14
|
uri;
|
|
@@ -30,8 +30,13 @@ __decorate([
|
|
|
30
30
|
], TotpRecoveryCodesResult.prototype, "recoveryCodes", void 0);
|
|
31
31
|
export class TotpStatusResult {
|
|
32
32
|
active;
|
|
33
|
+
remainingRecoveryCodes;
|
|
33
34
|
}
|
|
34
35
|
__decorate([
|
|
35
36
|
BooleanProperty(),
|
|
36
37
|
__metadata("design:type", Boolean)
|
|
37
38
|
], TotpStatusResult.prototype, "active", void 0);
|
|
39
|
+
__decorate([
|
|
40
|
+
NumberProperty({ nullable: true }),
|
|
41
|
+
__metadata("design:type", Object)
|
|
42
|
+
], TotpStatusResult.prototype, "remainingRecoveryCodes", void 0);
|
|
@@ -99,6 +99,7 @@ export declare class AuthenticationApiController<AdditionalTokenPayload extends
|
|
|
99
99
|
*/
|
|
100
100
|
invalidateAllOtherSessions({ getToken, getAuditor }: ApiRequestContext<AuthenticationApiDefinition<AdditionalTokenPayload, AuthenticationData, AdditionalInitPasswordResetData>, 'invalidateAllOtherSessions'>): Promise<ApiServerResult<AuthenticationApiDefinition<AdditionalTokenPayload, AuthenticationData, AdditionalInitPasswordResetData>, 'invalidateAllOtherSessions'>>;
|
|
101
101
|
protected enforceRateLimit(ip: string, subjectResource: string, auditor: Auditor, targetId: string, action: string): Promise<void>;
|
|
102
|
+
protected refundRateLimit(ip: string, subjectResource: string): Promise<void>;
|
|
102
103
|
protected getTokenResponse<T>(result: TokenResult<AdditionalTokenPayload>, body: NoInfer<T>): HttpServerResponse;
|
|
103
104
|
protected getLoginResponse(result: LoginResult<AdditionalTokenPayload>): ApiServerResult<AuthenticationApiDefinition<AdditionalTokenPayload, AuthenticationData, AdditionalInitPasswordResetData>, 'login'>;
|
|
104
105
|
}
|
|
@@ -35,12 +35,12 @@ let AuthenticationApiController = AuthenticationApiController_1 = class Authenti
|
|
|
35
35
|
authenticationService = inject((AuthenticationService));
|
|
36
36
|
options = inject(AuthenticationServiceOptions, undefined, { optional: true }) ?? {};
|
|
37
37
|
subjectRateLimiter = inject(RateLimiter, {
|
|
38
|
-
|
|
38
|
+
namespace: 'authentication:subject',
|
|
39
39
|
burstCapacity: this.options.bruteForceProtection?.subjectBurstCapacity ?? 10,
|
|
40
40
|
refillInterval: this.options.bruteForceProtection?.subjectRefillInterval ?? (30 * millisecondsPerMinute),
|
|
41
41
|
});
|
|
42
42
|
ipRateLimiter = inject(RateLimiter, {
|
|
43
|
-
|
|
43
|
+
namespace: 'authentication:ip',
|
|
44
44
|
burstCapacity: this.options.bruteForceProtection?.ipBurstCapacity ?? 20,
|
|
45
45
|
refillInterval: this.options.bruteForceProtection?.ipRefillInterval ?? (5 * millisecondsPerMinute),
|
|
46
46
|
});
|
|
@@ -154,6 +154,7 @@ let AuthenticationApiController = AuthenticationApiController_1 = class Authenti
|
|
|
154
154
|
const subjectResource = `${validatedToken.payload.tenant}:${validatedToken.payload.subject}`;
|
|
155
155
|
await this.enforceRateLimit(request.ip, subjectResource, auditor, validatedToken.payload.subject, 'refresh');
|
|
156
156
|
const result = await this.authenticationService.refreshAlreadyValidatedToken(validatedToken, parameters.data, undefined, auditor);
|
|
157
|
+
await this.refundRateLimit(request.ip, subjectResource);
|
|
157
158
|
return this.getTokenResponse(result, result.jsonToken.payload);
|
|
158
159
|
}
|
|
159
160
|
/**
|
|
@@ -332,6 +333,12 @@ let AuthenticationApiController = AuthenticationApiController_1 = class Authenti
|
|
|
332
333
|
throw new TooManyRequestsError();
|
|
333
334
|
}
|
|
334
335
|
}
|
|
336
|
+
async refundRateLimit(ip, subjectResource) {
|
|
337
|
+
await Promise.all([
|
|
338
|
+
this.ipRateLimiter.refund(ip),
|
|
339
|
+
this.subjectRateLimiter.refund(subjectResource),
|
|
340
|
+
]);
|
|
341
|
+
}
|
|
335
342
|
getTokenResponse(result, body) {
|
|
336
343
|
const { token, jsonToken, refreshToken, remember, omitImpersonatorRefreshToken, impersonatorRefreshToken, impersonatorRefreshTokenExpiration } = result;
|
|
337
344
|
const options = {
|
|
@@ -294,14 +294,15 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
|
|
|
294
294
|
const authAuditor = auditor.fork(AuthenticationService_1.name);
|
|
295
295
|
await this.#endSession(sessionId, authAuditor);
|
|
296
296
|
}
|
|
297
|
-
async #endSession(sessionId, authAuditor) {
|
|
298
|
-
const
|
|
297
|
+
async #endSession(sessionId, authAuditor, tx) {
|
|
298
|
+
const sessionRepository = this.#sessionRepository.withOptionalTransaction(tx);
|
|
299
|
+
const session = await sessionRepository.tryLoad(sessionId);
|
|
299
300
|
if (isUndefined(session)) {
|
|
300
301
|
this.#logger.warn(`Session "${sessionId}" not found for logout.`);
|
|
301
302
|
return;
|
|
302
303
|
}
|
|
303
304
|
const now = currentTimestamp();
|
|
304
|
-
await
|
|
305
|
+
await sessionRepository.update(sessionId, { end: now });
|
|
305
306
|
await authAuditor.info('logout', {
|
|
306
307
|
tenantId: session.tenantId,
|
|
307
308
|
actor: session.subjectId,
|
|
@@ -411,53 +412,55 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
|
|
|
411
412
|
let session;
|
|
412
413
|
try {
|
|
413
414
|
const sessionId = validatedRefreshToken.payload.session;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
415
|
+
return await this.#sessionRepository.transaction(async (tx) => {
|
|
416
|
+
session = await this.#sessionRepository.withTransaction(tx).load(sessionId);
|
|
417
|
+
const hash = await this.getHash(validatedRefreshToken.payload.secret, session.refreshTokenSalt);
|
|
418
|
+
if (session.end <= currentTimestamp()) {
|
|
419
|
+
throw new InvalidTokenError('Session is expired.');
|
|
420
|
+
}
|
|
421
|
+
if (!timingSafeBinaryEquals(hash, session.refreshTokenHash)) {
|
|
422
|
+
await this.#endSession(sessionId, authAuditor, tx);
|
|
423
|
+
await authAuditor.warn('refresh-failure', {
|
|
424
|
+
actorType: ActorType.Anonymous,
|
|
425
|
+
tenantId: session.tenantId,
|
|
426
|
+
targetId: session.subjectId,
|
|
427
|
+
targetType: 'Subject',
|
|
428
|
+
details: { sessionId, reason: 'Token reuse detected. Session revoked.' },
|
|
429
|
+
});
|
|
430
|
+
throw new InvalidTokenError('Invalid refresh token.');
|
|
431
|
+
}
|
|
432
|
+
const now = currentTimestamp();
|
|
433
|
+
const impersonator = (options.omitImpersonator == true) ? undefined : validatedRefreshToken.payload.impersonator;
|
|
434
|
+
const remember = validatedRefreshToken.payload.remember;
|
|
435
|
+
const ttl = remember ? this.rememberRefreshTokenTimeToLive : this.refreshTokenTimeToLive;
|
|
436
|
+
const newEnd = now + ttl;
|
|
437
|
+
const subject = await this.#subjectRepository.loadByQuery({ tenantId: session.tenantId, id: session.subjectId });
|
|
438
|
+
this.ensureNotSuspended(subject);
|
|
439
|
+
const tokenPayload = await this.#authenticationAncillaryService?.getTokenPayload(subject, authenticationData, { action: GetTokenPayloadContextAction.Refresh });
|
|
440
|
+
const { token, jsonToken } = await this.createToken({ additionalTokenPayload: tokenPayload, subject, sessionId, refreshTokenExpiration: newEnd, impersonator, timestamp: now });
|
|
441
|
+
const newRefreshToken = await this.createRefreshToken(subject, sessionId, newEnd, { impersonator, remember });
|
|
442
|
+
await this.#sessionRepository.withTransaction(tx).update(sessionId, {
|
|
443
|
+
end: newEnd,
|
|
444
|
+
refreshTokenSalt: newRefreshToken.salt,
|
|
445
|
+
refreshTokenHash: newRefreshToken.hash,
|
|
446
|
+
});
|
|
447
|
+
await authAuditor.info('refresh-success', {
|
|
423
448
|
tenantId: session.tenantId,
|
|
449
|
+
actor: session.subjectId,
|
|
450
|
+
actorType: ActorType.Subject,
|
|
424
451
|
targetId: session.subjectId,
|
|
425
452
|
targetType: 'Subject',
|
|
426
|
-
details: { sessionId,
|
|
453
|
+
details: { sessionId, remember },
|
|
427
454
|
});
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
const now = currentTimestamp();
|
|
431
|
-
const impersonator = (options.omitImpersonator == true) ? undefined : validatedRefreshToken.payload.impersonator;
|
|
432
|
-
const remember = validatedRefreshToken.payload.remember;
|
|
433
|
-
const ttl = remember ? this.rememberRefreshTokenTimeToLive : this.refreshTokenTimeToLive;
|
|
434
|
-
const newEnd = now + ttl;
|
|
435
|
-
const subject = await this.#subjectRepository.loadByQuery({ tenantId: session.tenantId, id: session.subjectId });
|
|
436
|
-
this.ensureNotSuspended(subject);
|
|
437
|
-
const tokenPayload = await this.#authenticationAncillaryService?.getTokenPayload(subject, authenticationData, { action: GetTokenPayloadContextAction.Refresh });
|
|
438
|
-
const { token, jsonToken } = await this.createToken({ additionalTokenPayload: tokenPayload, subject, sessionId, refreshTokenExpiration: newEnd, impersonator, timestamp: now });
|
|
439
|
-
const newRefreshToken = await this.createRefreshToken(subject, sessionId, newEnd, { impersonator, remember });
|
|
440
|
-
await this.#sessionRepository.update(sessionId, {
|
|
441
|
-
end: newEnd,
|
|
442
|
-
refreshTokenSalt: newRefreshToken.salt,
|
|
443
|
-
refreshTokenHash: newRefreshToken.hash,
|
|
444
|
-
});
|
|
445
|
-
await authAuditor.info('refresh-success', {
|
|
446
|
-
tenantId: session.tenantId,
|
|
447
|
-
actor: session.subjectId,
|
|
448
|
-
actorType: ActorType.Subject,
|
|
449
|
-
targetId: session.subjectId,
|
|
450
|
-
targetType: 'Subject',
|
|
451
|
-
details: { sessionId, remember },
|
|
455
|
+
return { token, jsonToken, refreshToken: newRefreshToken.token, remember, omitImpersonatorRefreshToken: options.omitImpersonator };
|
|
452
456
|
});
|
|
453
|
-
return { token, jsonToken, refreshToken: newRefreshToken.token, remember, omitImpersonatorRefreshToken: options.omitImpersonator };
|
|
454
457
|
}
|
|
455
458
|
catch (error) {
|
|
456
459
|
await authAuditor.warn('refresh-failure', {
|
|
457
460
|
actorType: ActorType.Anonymous,
|
|
458
461
|
targetId: session?.subjectId ?? NIL_UUID,
|
|
459
462
|
targetType: 'Subject',
|
|
460
|
-
details: { sessionId: null, reason: error.message },
|
|
463
|
+
details: { sessionId: session?.id ?? null, reason: error.message },
|
|
461
464
|
});
|
|
462
465
|
throw error;
|
|
463
466
|
}
|
|
@@ -861,8 +864,13 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
|
|
|
861
864
|
}
|
|
862
865
|
async getTotpStatus(tenantId, subjectId) {
|
|
863
866
|
const totp = await this.tryGetTotp(tenantId, subjectId);
|
|
867
|
+
const active = totp?.status == TotpStatus.Active;
|
|
868
|
+
const remainingRecoveryCodes = active
|
|
869
|
+
? await this.#totpRecoveryCodeRepository.countByQuery({ tenantId, totpId: totp.id, usedTimestamp: null })
|
|
870
|
+
: null;
|
|
864
871
|
return {
|
|
865
|
-
active
|
|
872
|
+
active,
|
|
873
|
+
remainingRecoveryCodes,
|
|
866
874
|
};
|
|
867
875
|
}
|
|
868
876
|
async initEnrollTotp(tenantId, subjectId, auditor) {
|
|
@@ -995,7 +1003,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
|
|
|
995
1003
|
}));
|
|
996
1004
|
await this.#totpRepository.transaction(async (transaction) => {
|
|
997
1005
|
const totpRecoveryCodeRepository = this.#totpRecoveryCodeRepository.withTransaction(transaction);
|
|
998
|
-
await totpRecoveryCodeRepository.
|
|
1006
|
+
await totpRecoveryCodeRepository.hardDeleteManyByQuery({ tenantId, totpId: totp.id });
|
|
999
1007
|
await totpRecoveryCodeRepository.insertMany(hashedRecoveryCodes);
|
|
1000
1008
|
if (options?.invalidateOtherSessions == true) {
|
|
1001
1009
|
await this.#invalidateAllOtherSessions(tenantId, subjectId, NIL_UUID, authAuditor);
|
|
@@ -1028,7 +1036,10 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
|
|
|
1028
1036
|
});
|
|
1029
1037
|
throw new ForbiddenError('Invalid TOTP token');
|
|
1030
1038
|
}
|
|
1031
|
-
await this.#totpRepository.
|
|
1039
|
+
await this.#totpRepository.transaction(async (tx) => {
|
|
1040
|
+
await this.#totpRecoveryCodeRepository.withTransaction(tx).hardDeleteManyByQuery({ tenantId, totpId: totp.id });
|
|
1041
|
+
await this.#totpRepository.withTransaction(tx).hardDeleteByQuery({ tenantId, id: totp.id });
|
|
1042
|
+
});
|
|
1032
1043
|
await authAuditor.info('totp-disable-success', {
|
|
1033
1044
|
tenantId,
|
|
1034
1045
|
actor: subjectId,
|
|
@@ -1054,7 +1065,10 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
|
|
|
1054
1065
|
});
|
|
1055
1066
|
throw new ForbiddenError('Invalid recovery code');
|
|
1056
1067
|
}
|
|
1057
|
-
await this.#totpRepository.
|
|
1068
|
+
await this.#totpRepository.transaction(async (tx) => {
|
|
1069
|
+
await this.#totpRecoveryCodeRepository.withTransaction(tx).hardDeleteManyByQuery({ tenantId, totpId: totp.id });
|
|
1070
|
+
await this.#totpRepository.withTransaction(tx).hardDeleteByQuery({ tenantId, id: totp.id });
|
|
1071
|
+
});
|
|
1058
1072
|
await authAuditor.info('totp-disable-success', {
|
|
1059
1073
|
tenantId,
|
|
1060
1074
|
actor: subjectId,
|
|
@@ -1075,58 +1089,63 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
|
|
|
1075
1089
|
const challengeToken = await this.validateTotpChallengeToken(challengeTokenString);
|
|
1076
1090
|
const { tenant, subject: subjectId, remember, data } = challengeToken.payload;
|
|
1077
1091
|
const totp = await this.#totpRepository.loadByQuery({ tenantId: tenant, subjectId });
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1092
|
+
const result = await this.#totpRepository.transaction(async (tx) => {
|
|
1093
|
+
const isRecoveryCodeValid = await this.verifyAndUseRecoveryCode(totp, recoveryCode, tx);
|
|
1094
|
+
if (!isRecoveryCodeValid) {
|
|
1095
|
+
await authAuditor.warn('recovery-login-failure', {
|
|
1096
|
+
actorType: ActorType.Anonymous,
|
|
1097
|
+
tenantId: tenant,
|
|
1098
|
+
targetId: subjectId,
|
|
1099
|
+
targetType: 'Subject',
|
|
1100
|
+
details: {
|
|
1101
|
+
subjectInput: { tenantId: tenant, subject: subjectId },
|
|
1102
|
+
resolvedSubjectId: subjectId,
|
|
1103
|
+
},
|
|
1104
|
+
});
|
|
1105
|
+
throw new ForbiddenError('Invalid recovery code');
|
|
1106
|
+
}
|
|
1107
|
+
const subject = await this.#subjectRepository.loadByQuery({ tenantId: tenant, id: subjectId });
|
|
1108
|
+
const loginResult = await this.#loginAlreadyValidatedSubject(subject, data, authAuditor, remember);
|
|
1109
|
+
const unusedRecoveryCodesCount = await this.#totpRecoveryCodeRepository.withTransaction(tx).countByQuery({ tenantId: tenant, totpId: totp.id, usedTimestamp: null });
|
|
1110
|
+
return { loginResult, unusedRecoveryCodesCount };
|
|
1111
|
+
});
|
|
1095
1112
|
await authAuditor.info('recovery-login-success', {
|
|
1096
1113
|
tenantId: tenant,
|
|
1097
1114
|
actor: subjectId,
|
|
1098
1115
|
actorType: ActorType.Subject,
|
|
1099
1116
|
targetId: subjectId,
|
|
1100
1117
|
targetType: 'Subject',
|
|
1101
|
-
network: { sessionId: loginResult.result.jsonToken.payload.session },
|
|
1118
|
+
network: { sessionId: result.loginResult.result.jsonToken.payload.session },
|
|
1102
1119
|
details: {
|
|
1103
|
-
sessionId: loginResult.result.jsonToken.payload.session,
|
|
1120
|
+
sessionId: result.loginResult.result.jsonToken.payload.session,
|
|
1104
1121
|
remember,
|
|
1105
1122
|
},
|
|
1106
1123
|
});
|
|
1107
1124
|
return {
|
|
1108
|
-
...loginResult,
|
|
1109
|
-
lowRecoveryCodesWarning: unusedRecoveryCodesCount <= 3,
|
|
1125
|
+
...result.loginResult,
|
|
1126
|
+
lowRecoveryCodesWarning: result.unusedRecoveryCodesCount <= 3,
|
|
1110
1127
|
};
|
|
1111
1128
|
}
|
|
1112
1129
|
async #loginVerifyTotp(challengeTokenString, token, authAuditor) {
|
|
1113
1130
|
const challengeToken = await this.validateTotpChallengeToken(challengeTokenString);
|
|
1114
1131
|
const { tenant, subject: subjectId, remember, data } = challengeToken.payload;
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1132
|
+
return await this.#totpRepository.transaction(async (tx) => {
|
|
1133
|
+
const totp = await this.#totpRepository.withTransaction(tx).loadByQuery({ tenantId: tenant, subjectId });
|
|
1134
|
+
const isValid = await this.verifyAndRecordTotpToken(totp, token, authAuditor, tx);
|
|
1135
|
+
if (!isValid) {
|
|
1136
|
+
await authAuditor.warn('totp-verify-failure', {
|
|
1137
|
+
tenantId: tenant,
|
|
1138
|
+
actor: subjectId,
|
|
1139
|
+
actorType: ActorType.Subject,
|
|
1140
|
+
targetId: subjectId,
|
|
1141
|
+
targetType: 'Subject',
|
|
1142
|
+
details: { reason: 'Invalid token' },
|
|
1143
|
+
});
|
|
1144
|
+
throw new ForbiddenError('Invalid TOTP token');
|
|
1145
|
+
}
|
|
1146
|
+
const subject = await this.#subjectRepository.loadByQuery({ tenantId: tenant, id: subjectId });
|
|
1147
|
+
return await this.#loginAlreadyValidatedSubject(subject, data, authAuditor, remember);
|
|
1148
|
+
});
|
|
1130
1149
|
}
|
|
1131
1150
|
async validateTotpChallengeToken(tokenString) {
|
|
1132
1151
|
const validatedToken = await parseAndValidateJwtTokenString(tokenString, 'KMAC256', await this.#totpChallengeSigningKey.getKey());
|
|
@@ -1155,26 +1174,29 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
|
|
|
1155
1174
|
};
|
|
1156
1175
|
return await createJwtTokenString(jsonToken, await this.#totpChallengeSigningKey.getKey());
|
|
1157
1176
|
}
|
|
1158
|
-
async verifyAndUseRecoveryCode(totp, code) {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1165
|
-
await
|
|
1166
|
-
|
|
1177
|
+
async verifyAndUseRecoveryCode(totp, code, tx) {
|
|
1178
|
+
return await this.#totpRecoveryCodeRepository.useTransaction(tx, async (tx) => {
|
|
1179
|
+
const totpRecoveryCodeRepository = this.#totpRecoveryCodeRepository.withTransaction(tx);
|
|
1180
|
+
const recoveryCodes = await totpRecoveryCodeRepository.loadManyByQuery({ tenantId: totp.tenantId, totpId: totp.id, usedTimestamp: null });
|
|
1181
|
+
const hashingOptions = this.#getTotpRecoveryCodeHashingOptions(totp.recoveryCodeSalt);
|
|
1182
|
+
const hash = await hashTotpRecoveryCode(code, hashingOptions);
|
|
1183
|
+
for (const recoveryCode of recoveryCodes) {
|
|
1184
|
+
const isValid = await verifyTotpRecoveryCode(hash, recoveryCode.code, hashingOptions);
|
|
1185
|
+
if (isValid) {
|
|
1186
|
+
await totpRecoveryCodeRepository.updateByQuery({ tenantId: recoveryCode.tenantId, id: recoveryCode.id, usedTimestamp: null }, { usedTimestamp: TRANSACTION_TIMESTAMP });
|
|
1187
|
+
return true;
|
|
1188
|
+
}
|
|
1167
1189
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1190
|
+
return false;
|
|
1191
|
+
});
|
|
1170
1192
|
}
|
|
1171
|
-
async verifyAndRecordTotpToken(totp, token, authAuditor) {
|
|
1193
|
+
async verifyAndRecordTotpToken(totp, token, authAuditor, tx) {
|
|
1172
1194
|
const secret = await importHmacKey('raw-secret', this.totpOptions.codeHashAlgorithm, totp.secret, false);
|
|
1173
1195
|
const isValid = await verifyTotpToken(secret, token, this.#getTotpOptions(totp.recoveryCodeSalt));
|
|
1174
1196
|
if (!isValid) {
|
|
1175
1197
|
return false;
|
|
1176
1198
|
}
|
|
1177
|
-
const inserted = await this.#usedTotpTokenRepository.tryInsert({
|
|
1199
|
+
const inserted = await this.#usedTotpTokenRepository.withOptionalTransaction(tx).tryInsert({
|
|
1178
1200
|
tenantId: totp.tenantId,
|
|
1179
1201
|
subjectId: totp.subjectId,
|
|
1180
1202
|
token,
|
|
@@ -140,4 +140,5 @@ ALTER TABLE "authentication"."used_totp_tokens" ADD CONSTRAINT "used_totp_tokens
|
|
|
140
140
|
ALTER TABLE "authentication"."service_account" ADD CONSTRAINT "service_account_tenantId_type_id_subject_fkey" FOREIGN KEY ("tenant_id","type","id") REFERENCES "authentication"."subject"("tenant_id","type","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
141
141
|
ALTER TABLE "authentication"."service_account" ADD CONSTRAINT "service_account_id_subject_fkey" FOREIGN KEY ("tenant_id","parent") REFERENCES "authentication"."subject"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
142
142
|
ALTER TABLE "authentication"."system_account" ADD CONSTRAINT "system_account_tenantId_type_id_subject_fkey" FOREIGN KEY ("tenant_id","type","id") REFERENCES "authentication"."subject"("tenant_id","type","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
143
|
-
ALTER TABLE "authentication"."user" ADD CONSTRAINT "user_tenantId_type_id_subject_fkey" FOREIGN KEY ("tenant_id","type","id") REFERENCES "authentication"."subject"("tenant_id","type","id") ON DELETE cascade ON UPDATE no action
|
|
143
|
+
ALTER TABLE "authentication"."user" ADD CONSTRAINT "user_tenantId_type_id_subject_fkey" FOREIGN KEY ("tenant_id","type","id") REFERENCES "authentication"."subject"("tenant_id","type","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
144
|
+
CREATE INDEX "totp_recovery_code_tenant_id_totp_id_idx" ON "authentication"."totp_recovery_code" USING btree ("tenant_id","totp_id");
|