auth-vir 3.0.1 → 3.1.1
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/README.md +7 -8
- package/dist/auth-client/backend-auth.client.d.ts +23 -0
- package/dist/auth-client/backend-auth.client.js +110 -17
- package/dist/auth-client/frontend-auth.client.d.ts +4 -2
- package/dist/auth-client/frontend-auth.client.js +14 -22
- package/dist/auth.d.ts +8 -7
- package/dist/auth.js +6 -10
- package/dist/cookie.js +3 -1
- package/dist/csrf-token-store.d.ts +21 -0
- package/dist/csrf-token-store.js +35 -0
- package/dist/csrf-token.d.ts +16 -15
- package/dist/csrf-token.js +13 -10
- package/dist/generated/internal/class.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/jwt/jwt.js +16 -5
- package/dist/jwt/user-jwt.js +3 -1
- package/dist/mock-csrf-token-store.d.ts +64 -0
- package/dist/mock-csrf-token-store.js +107 -0
- package/package.json +8 -7
- package/src/auth-client/backend-auth.client.ts +169 -26
- package/src/auth-client/frontend-auth.client.ts +15 -25
- package/src/auth.ts +9 -15
- package/src/cookie.ts +3 -1
- package/src/csrf-token-store.ts +54 -0
- package/src/csrf-token.ts +25 -25
- package/src/generated/internal/class.ts +2 -2
- package/src/index.ts +2 -1
- package/src/jwt/jwt.ts +16 -5
- package/src/jwt/user-jwt.ts +3 -1
- package/src/mock-csrf-token-store.ts +141 -0
- package/dist/mock-local-storage.d.ts +0 -33
- package/dist/mock-local-storage.js +0 -56
- package/src/mock-local-storage.ts +0 -72
|
@@ -91,6 +91,12 @@ export type BackendAuthClientConfig<
|
|
|
91
91
|
*/
|
|
92
92
|
isDev: boolean;
|
|
93
93
|
} & PartialWithUndefined<{
|
|
94
|
+
/** If this returns true, logging will be enabled while handling the relevant session. */
|
|
95
|
+
enableLogging(params: {
|
|
96
|
+
user: DatabaseUser | undefined;
|
|
97
|
+
userId: UserId | undefined;
|
|
98
|
+
assumedUserParams: AssumedUserParams | undefined;
|
|
99
|
+
}): boolean;
|
|
94
100
|
/**
|
|
95
101
|
* Overwrite the header name used for tracking is an admin is assuming the identity of
|
|
96
102
|
* another user.
|
|
@@ -103,6 +109,8 @@ export type BackendAuthClientConfig<
|
|
|
103
109
|
generateServiceOrigin(params: {
|
|
104
110
|
requestHeaders: Readonly<IncomingHttpHeaders>;
|
|
105
111
|
}): MaybePromise<undefined | string>;
|
|
112
|
+
/** If provided, logs will be sent to this method. */
|
|
113
|
+
log?: (message: string, extraData: AnyObject) => void;
|
|
106
114
|
/**
|
|
107
115
|
* Set this to allow specific users (determined by `canAssumeUser`) to assume the identity
|
|
108
116
|
* of other users. This should only be used for admins so that they can troubleshoot user
|
|
@@ -197,6 +205,30 @@ export class BackendAuthClient<
|
|
|
197
205
|
protected readonly config: BackendAuthClientConfig<DatabaseUser, UserId, AssumedUserParams>,
|
|
198
206
|
) {}
|
|
199
207
|
|
|
208
|
+
/** Conditionally logs a message if logging is enabled for the given user context. */
|
|
209
|
+
protected logForUser(
|
|
210
|
+
params: {
|
|
211
|
+
user: DatabaseUser | undefined;
|
|
212
|
+
userId: UserId | undefined;
|
|
213
|
+
assumedUserParams: AssumedUserParams | undefined;
|
|
214
|
+
},
|
|
215
|
+
message: string,
|
|
216
|
+
extra?: Record<string, unknown>,
|
|
217
|
+
): void {
|
|
218
|
+
if (this.config.enableLogging?.(params)) {
|
|
219
|
+
const extraData = {
|
|
220
|
+
userId: params.userId,
|
|
221
|
+
...extra,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (this.config.log) {
|
|
225
|
+
this.config.log(message, extraData);
|
|
226
|
+
} else {
|
|
227
|
+
console.info(`[auth-vir] ${message}`, extraData);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
200
232
|
/** Get all the parameters used for cookie generation. */
|
|
201
233
|
protected async getCookieParams({
|
|
202
234
|
isSignUpCookie,
|
|
@@ -212,7 +244,9 @@ export class BackendAuthClient<
|
|
|
212
244
|
requestHeaders: Readonly<IncomingHttpHeaders> | undefined;
|
|
213
245
|
}): Promise<Readonly<CookieParams>> {
|
|
214
246
|
const serviceOrigin = requestHeaders
|
|
215
|
-
? await this.config.generateServiceOrigin?.({
|
|
247
|
+
? await this.config.generateServiceOrigin?.({
|
|
248
|
+
requestHeaders,
|
|
249
|
+
})
|
|
216
250
|
: undefined;
|
|
217
251
|
|
|
218
252
|
return {
|
|
@@ -248,6 +282,17 @@ export class BackendAuthClient<
|
|
|
248
282
|
});
|
|
249
283
|
|
|
250
284
|
if (!authenticatedUser) {
|
|
285
|
+
this.logForUser(
|
|
286
|
+
{
|
|
287
|
+
user: undefined,
|
|
288
|
+
userId,
|
|
289
|
+
assumedUserParams: assumingUser,
|
|
290
|
+
},
|
|
291
|
+
'getUserFromDatabase returned no user',
|
|
292
|
+
{
|
|
293
|
+
isSignUpCookie,
|
|
294
|
+
},
|
|
295
|
+
);
|
|
251
296
|
return undefined;
|
|
252
297
|
}
|
|
253
298
|
|
|
@@ -273,6 +318,18 @@ export class BackendAuthClient<
|
|
|
273
318
|
});
|
|
274
319
|
|
|
275
320
|
if (isExpiredAlready) {
|
|
321
|
+
this.logForUser(
|
|
322
|
+
{
|
|
323
|
+
user: undefined,
|
|
324
|
+
userId: userIdResult.userId,
|
|
325
|
+
assumedUserParams: undefined,
|
|
326
|
+
},
|
|
327
|
+
'Session refresh denied: JWT already expired (even with clock skew tolerance)',
|
|
328
|
+
{
|
|
329
|
+
jwtExpiration: userIdResult.jwtExpiration,
|
|
330
|
+
now: JSON.stringify(now),
|
|
331
|
+
},
|
|
332
|
+
);
|
|
276
333
|
return undefined;
|
|
277
334
|
}
|
|
278
335
|
|
|
@@ -290,6 +347,19 @@ export class BackendAuthClient<
|
|
|
290
347
|
});
|
|
291
348
|
|
|
292
349
|
if (isSessionExpired) {
|
|
350
|
+
this.logForUser(
|
|
351
|
+
{
|
|
352
|
+
user: undefined,
|
|
353
|
+
userId: userIdResult.userId,
|
|
354
|
+
assumedUserParams: undefined,
|
|
355
|
+
},
|
|
356
|
+
'Session refresh denied: max session duration exceeded',
|
|
357
|
+
{
|
|
358
|
+
sessionStartedAt: userIdResult.sessionStartedAt,
|
|
359
|
+
maxSessionEndDate: JSON.stringify(maxSessionEndDate),
|
|
360
|
+
now: JSON.stringify(now),
|
|
361
|
+
},
|
|
362
|
+
);
|
|
293
363
|
return undefined;
|
|
294
364
|
}
|
|
295
365
|
}
|
|
@@ -327,6 +397,18 @@ export class BackendAuthClient<
|
|
|
327
397
|
}),
|
|
328
398
|
};
|
|
329
399
|
} else {
|
|
400
|
+
this.logForUser(
|
|
401
|
+
{
|
|
402
|
+
user: undefined,
|
|
403
|
+
userId: userIdResult.userId,
|
|
404
|
+
assumedUserParams: undefined,
|
|
405
|
+
},
|
|
406
|
+
'Session refresh skipped: not yet ready for refresh',
|
|
407
|
+
{
|
|
408
|
+
jwtIssuedAt: userIdResult.jwtIssuedAt,
|
|
409
|
+
sessionRefreshStartTime,
|
|
410
|
+
},
|
|
411
|
+
);
|
|
330
412
|
return undefined;
|
|
331
413
|
}
|
|
332
414
|
}
|
|
@@ -390,6 +472,17 @@ export class BackendAuthClient<
|
|
|
390
472
|
isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth,
|
|
391
473
|
);
|
|
392
474
|
if (!userIdResult) {
|
|
475
|
+
this.logForUser(
|
|
476
|
+
{
|
|
477
|
+
user: undefined,
|
|
478
|
+
userId: undefined,
|
|
479
|
+
assumedUserParams: undefined,
|
|
480
|
+
},
|
|
481
|
+
'getSecureUser: failed to extract user ID from request headers (invalid JWT, missing cookie, or CSRF mismatch)',
|
|
482
|
+
{
|
|
483
|
+
isSignUpCookie,
|
|
484
|
+
},
|
|
485
|
+
);
|
|
393
486
|
return undefined;
|
|
394
487
|
}
|
|
395
488
|
|
|
@@ -401,6 +494,17 @@ export class BackendAuthClient<
|
|
|
401
494
|
});
|
|
402
495
|
|
|
403
496
|
if (!user) {
|
|
497
|
+
this.logForUser(
|
|
498
|
+
{
|
|
499
|
+
user: undefined,
|
|
500
|
+
userId: userIdResult.userId,
|
|
501
|
+
assumedUserParams: undefined,
|
|
502
|
+
},
|
|
503
|
+
'getSecureUser: user not found in database',
|
|
504
|
+
{
|
|
505
|
+
isSignUpCookie,
|
|
506
|
+
},
|
|
507
|
+
);
|
|
404
508
|
return undefined;
|
|
405
509
|
}
|
|
406
510
|
|
|
@@ -435,7 +539,9 @@ export class BackendAuthClient<
|
|
|
435
539
|
const parsedKeys = cachedParsedKeys || (await parseJwtKeys(rawJwtKeys));
|
|
436
540
|
|
|
437
541
|
if (!cachedParsedKeys) {
|
|
438
|
-
this.cachedParsedJwtKeys = {
|
|
542
|
+
this.cachedParsedJwtKeys = {
|
|
543
|
+
[cacheKey]: parsedKeys,
|
|
544
|
+
};
|
|
439
545
|
}
|
|
440
546
|
return {
|
|
441
547
|
jwtKeys: parsedKeys,
|
|
@@ -469,7 +575,6 @@ export class BackendAuthClient<
|
|
|
469
575
|
isSignUpCookie: true,
|
|
470
576
|
requestHeaders: undefined,
|
|
471
577
|
}),
|
|
472
|
-
this.config.csrf,
|
|
473
578
|
)
|
|
474
579
|
: undefined;
|
|
475
580
|
const authCookieHeaders =
|
|
@@ -479,26 +584,41 @@ export class BackendAuthClient<
|
|
|
479
584
|
isSignUpCookie: false,
|
|
480
585
|
requestHeaders: undefined,
|
|
481
586
|
}),
|
|
482
|
-
this.config.csrf,
|
|
483
587
|
)
|
|
484
588
|
: undefined;
|
|
485
589
|
|
|
486
|
-
|
|
487
|
-
'set-cookie': string[];
|
|
488
|
-
} = {
|
|
590
|
+
return {
|
|
489
591
|
'set-cookie': mergeHeaderValues(
|
|
490
592
|
signUpCookieHeaders?.['set-cookie'],
|
|
491
593
|
authCookieHeaders?.['set-cookie'],
|
|
492
594
|
),
|
|
493
595
|
};
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Refreshes a login session by reissuing the auth cookie with the same CSRF token instead of
|
|
600
|
+
* generating a new one.
|
|
601
|
+
*/
|
|
602
|
+
protected async refreshLoginHeaders({
|
|
603
|
+
userId,
|
|
604
|
+
cookieParams,
|
|
605
|
+
existingUserIdResult,
|
|
606
|
+
}: {
|
|
607
|
+
userId: UserId;
|
|
608
|
+
cookieParams: Readonly<CookieParams>;
|
|
609
|
+
existingUserIdResult: Readonly<UserIdResult<UserId>>;
|
|
610
|
+
}): Promise<Record<string, string>> {
|
|
611
|
+
const {cookie} = await generateAuthCookie(
|
|
612
|
+
{
|
|
613
|
+
csrfToken: existingUserIdResult.csrfToken,
|
|
614
|
+
userId,
|
|
615
|
+
sessionStartedAt: existingUserIdResult.sessionStartedAt,
|
|
616
|
+
},
|
|
617
|
+
cookieParams,
|
|
618
|
+
);
|
|
498
619
|
|
|
499
620
|
return {
|
|
500
|
-
|
|
501
|
-
...setCookieHeader,
|
|
621
|
+
'set-cookie': cookie,
|
|
502
622
|
};
|
|
503
623
|
}
|
|
504
624
|
|
|
@@ -521,7 +641,6 @@ export class BackendAuthClient<
|
|
|
521
641
|
isSignUpCookie: !isSignUpCookie,
|
|
522
642
|
requestHeaders,
|
|
523
643
|
}),
|
|
524
|
-
this.config.csrf,
|
|
525
644
|
)
|
|
526
645
|
: undefined;
|
|
527
646
|
|
|
@@ -531,17 +650,19 @@ export class BackendAuthClient<
|
|
|
531
650
|
this.config.csrf,
|
|
532
651
|
isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth,
|
|
533
652
|
);
|
|
534
|
-
const sessionStartedAt = existingUserIdResult?.sessionStartedAt;
|
|
535
653
|
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
this.
|
|
543
|
-
|
|
544
|
-
|
|
654
|
+
const cookieParams = await this.getCookieParams({
|
|
655
|
+
isSignUpCookie,
|
|
656
|
+
requestHeaders,
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
const newCookieHeaders = existingUserIdResult
|
|
660
|
+
? await this.refreshLoginHeaders({
|
|
661
|
+
userId,
|
|
662
|
+
cookieParams,
|
|
663
|
+
existingUserIdResult,
|
|
664
|
+
})
|
|
665
|
+
: await generateSuccessfulLoginHeaders(userId, cookieParams, this.config.csrf);
|
|
545
666
|
|
|
546
667
|
return {
|
|
547
668
|
...newCookieHeaders,
|
|
@@ -584,13 +705,19 @@ export class BackendAuthClient<
|
|
|
584
705
|
const secureUser = await this.getSecureUser(params);
|
|
585
706
|
|
|
586
707
|
if (secureUser) {
|
|
587
|
-
return {
|
|
708
|
+
return {
|
|
709
|
+
secureUser,
|
|
710
|
+
};
|
|
588
711
|
}
|
|
589
712
|
|
|
590
713
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
591
714
|
const insecureUser = await this.getInsecureUser(params);
|
|
592
715
|
|
|
593
|
-
return insecureUser
|
|
716
|
+
return insecureUser
|
|
717
|
+
? {
|
|
718
|
+
insecureUser,
|
|
719
|
+
}
|
|
720
|
+
: {};
|
|
594
721
|
}
|
|
595
722
|
|
|
596
723
|
/**
|
|
@@ -618,6 +745,14 @@ export class BackendAuthClient<
|
|
|
618
745
|
);
|
|
619
746
|
|
|
620
747
|
if (!userIdResult) {
|
|
748
|
+
this.logForUser(
|
|
749
|
+
{
|
|
750
|
+
user: undefined,
|
|
751
|
+
userId: undefined,
|
|
752
|
+
assumedUserParams: undefined,
|
|
753
|
+
},
|
|
754
|
+
'getInsecureUser: failed to extract user ID from cookie (invalid JWT or missing cookie)',
|
|
755
|
+
);
|
|
621
756
|
return undefined;
|
|
622
757
|
}
|
|
623
758
|
|
|
@@ -629,6 +764,14 @@ export class BackendAuthClient<
|
|
|
629
764
|
});
|
|
630
765
|
|
|
631
766
|
if (!user) {
|
|
767
|
+
this.logForUser(
|
|
768
|
+
{
|
|
769
|
+
user: undefined,
|
|
770
|
+
userId: userIdResult.userId,
|
|
771
|
+
assumedUserParams: undefined,
|
|
772
|
+
},
|
|
773
|
+
'getInsecureUser: user not found in database',
|
|
774
|
+
);
|
|
632
775
|
return undefined;
|
|
633
776
|
}
|
|
634
777
|
|
|
@@ -9,15 +9,14 @@ import {
|
|
|
9
9
|
import {type AnyDuration} from 'date-vir';
|
|
10
10
|
import {listenToActivity} from 'detect-activity';
|
|
11
11
|
import {type EmptyObject} from 'type-fest';
|
|
12
|
+
import {type CsrfTokenStore} from '../csrf-token-store.js';
|
|
12
13
|
import {
|
|
13
14
|
type CsrfHeaderNameOption,
|
|
14
|
-
CsrfTokenFailureReason,
|
|
15
15
|
defaultAllowedClockSkew,
|
|
16
16
|
extractCsrfTokenHeader,
|
|
17
17
|
getCurrentCsrfToken,
|
|
18
18
|
resolveCsrfHeaderName,
|
|
19
19
|
storeCsrfToken,
|
|
20
|
-
wipeCurrentCsrfToken,
|
|
21
20
|
} from '../csrf-token.js';
|
|
22
21
|
import {AuthHeaderName} from '../headers.js';
|
|
23
22
|
|
|
@@ -85,6 +84,7 @@ export type FrontendAuthClientConfig = Readonly<{
|
|
|
85
84
|
|
|
86
85
|
overrides: PartialWithUndefined<{
|
|
87
86
|
localStorage: Pick<Storage, 'setItem' | 'removeItem' | 'getItem'>;
|
|
87
|
+
csrfTokenStore: CsrfTokenStore;
|
|
88
88
|
}>;
|
|
89
89
|
}>;
|
|
90
90
|
|
|
@@ -112,7 +112,9 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
|
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
114
|
},
|
|
115
|
-
debounce: config.checkUser.debounce || {
|
|
115
|
+
debounce: config.checkUser.debounce || {
|
|
116
|
+
minutes: 1,
|
|
117
|
+
},
|
|
116
118
|
fireImmediately: false,
|
|
117
119
|
});
|
|
118
120
|
}
|
|
@@ -128,20 +130,14 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
|
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
/** Wraps {@link getCurrentCsrfToken} to automatically handle wiping an invalid CSRF token. */
|
|
131
|
-
public getCurrentCsrfToken(): string | undefined {
|
|
132
|
-
const csrfTokenResult = getCurrentCsrfToken({
|
|
133
|
+
public async getCurrentCsrfToken(): Promise<string | undefined> {
|
|
134
|
+
const csrfTokenResult = await getCurrentCsrfToken({
|
|
133
135
|
...this.config.csrf,
|
|
134
|
-
|
|
136
|
+
csrfTokenStore: this.config.overrides?.csrfTokenStore,
|
|
135
137
|
allowedClockSkew: this.config.allowedClockSkew || defaultAllowedClockSkew,
|
|
136
138
|
});
|
|
137
139
|
|
|
138
140
|
if (csrfTokenResult.failure) {
|
|
139
|
-
if (csrfTokenResult.failure !== CsrfTokenFailureReason.DoesNotExist) {
|
|
140
|
-
wipeCurrentCsrfToken({
|
|
141
|
-
...this.config.csrf,
|
|
142
|
-
localStorage: this.config.overrides?.localStorage,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
141
|
return undefined;
|
|
146
142
|
}
|
|
147
143
|
|
|
@@ -162,9 +158,7 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
|
|
|
162
158
|
if (!assumedUserParams) {
|
|
163
159
|
localStorage.removeItem(storageKey);
|
|
164
160
|
return true;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (!(await this.config.canAssumeUser?.())) {
|
|
161
|
+
} else if (!(await this.config.canAssumeUser?.())) {
|
|
168
162
|
return false;
|
|
169
163
|
}
|
|
170
164
|
|
|
@@ -195,8 +189,8 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
|
|
|
195
189
|
* `@augment-vir/common`](https://electrovir.github.io/augment-vir/functions/mergeDeep.html) to
|
|
196
190
|
* combine them with these.
|
|
197
191
|
*/
|
|
198
|
-
public createAuthenticatedRequestInit(): RequestInit {
|
|
199
|
-
const csrfToken = this.getCurrentCsrfToken();
|
|
192
|
+
public async createAuthenticatedRequestInit(): Promise<RequestInit> {
|
|
193
|
+
const csrfToken = await this.getCurrentCsrfToken();
|
|
200
194
|
|
|
201
195
|
const assumedUser = this.getAssumedUser();
|
|
202
196
|
const headers: HeadersInit = {
|
|
@@ -222,10 +216,6 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
|
|
|
222
216
|
/** Wipes the current user auth. */
|
|
223
217
|
public async logout() {
|
|
224
218
|
await this.config.authClearedCallback?.();
|
|
225
|
-
wipeCurrentCsrfToken({
|
|
226
|
-
...this.config.csrf,
|
|
227
|
-
localStorage: this.config.overrides?.localStorage,
|
|
228
|
-
});
|
|
229
219
|
}
|
|
230
220
|
|
|
231
221
|
/**
|
|
@@ -259,9 +249,9 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
|
|
|
259
249
|
throw new Error('Did not receive any CSRF token.');
|
|
260
250
|
}
|
|
261
251
|
|
|
262
|
-
storeCsrfToken(csrfToken, {
|
|
252
|
+
await storeCsrfToken(csrfToken, {
|
|
263
253
|
...this.config.csrf,
|
|
264
|
-
|
|
254
|
+
csrfTokenStore: this.config.overrides?.csrfTokenStore,
|
|
265
255
|
});
|
|
266
256
|
}
|
|
267
257
|
|
|
@@ -297,9 +287,9 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
|
|
|
297
287
|
allowedClockSkew: this.config.allowedClockSkew || defaultAllowedClockSkew,
|
|
298
288
|
});
|
|
299
289
|
if (csrfToken) {
|
|
300
|
-
storeCsrfToken(csrfToken, {
|
|
290
|
+
await storeCsrfToken(csrfToken, {
|
|
301
291
|
...this.config.csrf,
|
|
302
|
-
|
|
292
|
+
csrfTokenStore: this.config.overrides?.csrfTokenStore,
|
|
303
293
|
});
|
|
304
294
|
}
|
|
305
295
|
|
package/src/auth.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
extractCookieJwt,
|
|
8
8
|
generateAuthCookie,
|
|
9
9
|
} from './cookie.js';
|
|
10
|
+
import {type CsrfTokenStore} from './csrf-token-store.js';
|
|
10
11
|
import {
|
|
11
12
|
type CsrfHeaderNameOption,
|
|
12
13
|
extractCsrfTokenHeader,
|
|
@@ -14,7 +15,6 @@ import {
|
|
|
14
15
|
parseCsrfToken,
|
|
15
16
|
resolveCsrfHeaderName,
|
|
16
17
|
storeCsrfToken,
|
|
17
|
-
wipeCurrentCsrfToken,
|
|
18
18
|
} from './csrf-token.js';
|
|
19
19
|
import {type ParseJwtParams} from './jwt/jwt.js';
|
|
20
20
|
import {type JwtUserData} from './jwt/user-jwt.js';
|
|
@@ -202,48 +202,42 @@ export async function generateSuccessfulLoginHeaders(
|
|
|
202
202
|
*/
|
|
203
203
|
export function generateLogoutHeaders(
|
|
204
204
|
cookieConfig: Readonly<Pick<CookieParams, 'cookieName' | 'hostOrigin' | 'isDev'>>,
|
|
205
|
-
csrfHeaderNameOption: Readonly<CsrfHeaderNameOption>,
|
|
206
205
|
): Record<string, string> {
|
|
207
|
-
const csrfHeaderName = resolveCsrfHeaderName(csrfHeaderNameOption);
|
|
208
|
-
|
|
209
206
|
return {
|
|
210
207
|
'set-cookie': clearAuthCookie(cookieConfig),
|
|
211
|
-
[csrfHeaderName]: 'redacted',
|
|
212
208
|
};
|
|
213
209
|
}
|
|
214
210
|
|
|
215
211
|
/**
|
|
216
212
|
* Store auth data on a client (frontend) after receiving an auth response from the host (backend).
|
|
217
|
-
* Specifically, this stores the CSRF token into
|
|
218
|
-
* Alternatively, if the given response failed, this will wipe the existing (if
|
|
213
|
+
* Specifically, this stores the CSRF token into IndexedDB (which doesn't need to be a secret).
|
|
214
|
+
* Alternatively, if the given response failed, this will wipe the existing (if any) stored CSRF
|
|
219
215
|
* token.
|
|
220
216
|
*
|
|
221
217
|
* @category Auth : Client
|
|
222
218
|
* @throws Error if no CSRF token header is found.
|
|
223
219
|
*/
|
|
224
|
-
export function handleAuthResponse(
|
|
220
|
+
export async function handleAuthResponse(
|
|
225
221
|
response: Readonly<Pick<Response, 'ok' | 'headers'>>,
|
|
226
222
|
options: Readonly<CsrfHeaderNameOption> &
|
|
227
223
|
PartialWithUndefined<{
|
|
228
224
|
/**
|
|
229
|
-
* Allows mocking or overriding the
|
|
225
|
+
* Allows mocking or overriding the default CSRF token store.
|
|
230
226
|
*
|
|
231
|
-
* @default
|
|
227
|
+
* @default getDefaultCsrfTokenStore()
|
|
232
228
|
*/
|
|
233
|
-
|
|
229
|
+
csrfTokenStore: CsrfTokenStore;
|
|
234
230
|
}>,
|
|
235
|
-
) {
|
|
231
|
+
): Promise<void> {
|
|
236
232
|
if (!response.ok) {
|
|
237
|
-
wipeCurrentCsrfToken(options);
|
|
238
233
|
return;
|
|
239
234
|
}
|
|
240
235
|
|
|
241
236
|
const {csrfToken} = extractCsrfTokenHeader(response, options);
|
|
242
237
|
|
|
243
238
|
if (!csrfToken) {
|
|
244
|
-
wipeCurrentCsrfToken(options);
|
|
245
239
|
throw new Error('Did not receive any CSRF token.');
|
|
246
240
|
}
|
|
247
241
|
|
|
248
|
-
storeCsrfToken(csrfToken, options);
|
|
242
|
+
await storeCsrfToken(csrfToken, options);
|
|
249
243
|
}
|
package/src/cookie.ts
CHANGED
|
@@ -91,7 +91,9 @@ export async function generateAuthCookie(
|
|
|
91
91
|
HttpOnly: true,
|
|
92
92
|
Path: '/',
|
|
93
93
|
SameSite: 'Strict',
|
|
94
|
-
'MAX-AGE': convertDuration(cookieConfig.cookieDuration, {
|
|
94
|
+
'MAX-AGE': convertDuration(cookieConfig.cookieDuration, {
|
|
95
|
+
seconds: true,
|
|
96
|
+
}).seconds,
|
|
95
97
|
Secure: !cookieConfig.isDev,
|
|
96
98
|
}),
|
|
97
99
|
expiration,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {LocalDbClient} from 'local-db-client';
|
|
2
|
+
import {defineShape} from 'object-shape-tester';
|
|
3
|
+
|
|
4
|
+
const csrfTokenDbShapes = {
|
|
5
|
+
csrfToken: defineShape(''),
|
|
6
|
+
} as const;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The interface used for overriding the default CSRF token store in storage functions.
|
|
10
|
+
*
|
|
11
|
+
* @category Internal
|
|
12
|
+
*/
|
|
13
|
+
export type CsrfTokenStore = {
|
|
14
|
+
/** Retrieves the stored CSRF token, if any. */
|
|
15
|
+
getCsrfToken(): Promise<string | undefined>;
|
|
16
|
+
/** Stores a CSRF token. */
|
|
17
|
+
setCsrfToken(value: string): Promise<void>;
|
|
18
|
+
/** Deletes the stored CSRF token. */
|
|
19
|
+
deleteCsrfToken(): Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
async function createDefaultCsrfTokenStore(): Promise<CsrfTokenStore> {
|
|
23
|
+
const client = await LocalDbClient.createClient(csrfTokenDbShapes, {
|
|
24
|
+
storeName: 'auth-vir-csrf',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
async getCsrfToken() {
|
|
29
|
+
return (await client.load.csrfToken()) || undefined;
|
|
30
|
+
},
|
|
31
|
+
async setCsrfToken(value) {
|
|
32
|
+
await client.set.csrfToken(value);
|
|
33
|
+
},
|
|
34
|
+
async deleteCsrfToken() {
|
|
35
|
+
await client.delete.csrfToken();
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The default {@link LocalDbClient} instance used for storing CSRF tokens. This uses a dedicated
|
|
42
|
+
* store name to avoid collisions with other storage. Lazily initialized to avoid crashes in Node.js
|
|
43
|
+
* environments where IndexedDB is not available.
|
|
44
|
+
*
|
|
45
|
+
* @category Internal
|
|
46
|
+
*/
|
|
47
|
+
export async function getDefaultCsrfTokenStore(): Promise<CsrfTokenStore> {
|
|
48
|
+
if (!cachedStorePromise) {
|
|
49
|
+
cachedStorePromise = createDefaultCsrfTokenStore();
|
|
50
|
+
}
|
|
51
|
+
return cachedStorePromise;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let cachedStorePromise: Promise<CsrfTokenStore> | undefined;
|