auth-vir 2.7.2 → 3.0.0
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 +33 -11
- package/dist/auth-client/backend-auth.client.d.ts +24 -15
- package/dist/auth-client/backend-auth.client.js +40 -61
- package/dist/auth-client/frontend-auth.client.d.ts +24 -11
- package/dist/auth-client/frontend-auth.client.js +41 -32
- package/dist/auth-client/is-session-refresh-ready.d.ts +23 -0
- package/dist/auth-client/is-session-refresh-ready.js +21 -0
- package/dist/auth.d.ts +11 -19
- package/dist/auth.js +30 -49
- package/dist/cookie.d.ts +11 -2
- package/dist/cookie.js +16 -12
- package/dist/csrf-token.d.ts +52 -12
- package/dist/csrf-token.js +40 -15
- package/dist/hash.js +6 -4
- package/dist/headers.d.ts +0 -1
- package/dist/headers.js +0 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/jwt/jwt.d.ts +11 -1
- package/dist/jwt/jwt.js +9 -2
- package/dist/jwt/user-jwt.js +2 -1
- package/package.json +11 -11
- package/src/auth-client/backend-auth.client.ts +74 -94
- package/src/auth-client/frontend-auth.client.ts +97 -73
- package/src/auth-client/is-session-refresh-ready.ts +40 -0
- package/src/auth.ts +53 -99
- package/src/cookie.ts +38 -16
- package/src/csrf-token.ts +109 -48
- package/src/hash.ts +7 -4
- package/src/headers.ts +0 -1
- package/src/index.ts +1 -1
- package/src/jwt/jwt.ts +27 -2
- package/src/jwt/user-jwt.ts +2 -1
- package/dist/log.d.ts +0 -12
- package/dist/log.js +0 -20
- package/src/log.ts +0 -22
package/README.md
CHANGED
|
@@ -81,10 +81,19 @@ import {
|
|
|
81
81
|
parseJwtKeys,
|
|
82
82
|
type CookieParams,
|
|
83
83
|
type CreateJwtParams,
|
|
84
|
+
type CsrfHeaderNameOption,
|
|
84
85
|
} from 'auth-vir';
|
|
85
86
|
|
|
86
87
|
type MyUserId = string;
|
|
87
88
|
|
|
89
|
+
/**
|
|
90
|
+
* The CSRF header prefix for this app. Either `csrfHeaderPrefix` or `csrfHeaderName` must be
|
|
91
|
+
* provided to all CSRF-related functions.
|
|
92
|
+
*/
|
|
93
|
+
const csrfOption: CsrfHeaderNameOption = {
|
|
94
|
+
csrfHeaderPrefix: 'my-app',
|
|
95
|
+
};
|
|
96
|
+
|
|
88
97
|
/**
|
|
89
98
|
* Use this for a /login endpoint.
|
|
90
99
|
*
|
|
@@ -105,7 +114,7 @@ export async function handleLogin(
|
|
|
105
114
|
throw new Error('Credentials mismatch.');
|
|
106
115
|
}
|
|
107
116
|
|
|
108
|
-
const authHeaders = await generateSuccessfulLoginHeaders(user.id, cookieParams);
|
|
117
|
+
const authHeaders = await generateSuccessfulLoginHeaders(user.id, cookieParams, csrfOption);
|
|
109
118
|
response.setHeaders(new Headers(authHeaders));
|
|
110
119
|
}
|
|
111
120
|
|
|
@@ -121,7 +130,7 @@ export async function createUser(
|
|
|
121
130
|
) {
|
|
122
131
|
const newUser = await createUserInDatabase(userRequestData);
|
|
123
132
|
|
|
124
|
-
const authHeaders = await generateSuccessfulLoginHeaders(newUser.id, cookieParams);
|
|
133
|
+
const authHeaders = await generateSuccessfulLoginHeaders(newUser.id, cookieParams, csrfOption);
|
|
125
134
|
response.setHeaders(new Headers(authHeaders));
|
|
126
135
|
}
|
|
127
136
|
|
|
@@ -132,7 +141,7 @@ export async function createUser(
|
|
|
132
141
|
*/
|
|
133
142
|
export async function getAuthenticatedUser(request: ClientRequest) {
|
|
134
143
|
const userId = (
|
|
135
|
-
await extractUserIdFromRequestHeaders<MyUserId>(request.getHeaders(), jwtParams)
|
|
144
|
+
await extractUserIdFromRequestHeaders<MyUserId>(request.getHeaders(), jwtParams, csrfOption)
|
|
136
145
|
)?.userId;
|
|
137
146
|
const user = userId ? findUserInDatabaseById(userId) : undefined;
|
|
138
147
|
|
|
@@ -252,15 +261,28 @@ Here's a full example of how to use all the client / frontend side auth function
|
|
|
252
261
|
|
|
253
262
|
```TypeScript
|
|
254
263
|
import {HttpStatus} from '@augment-vir/common';
|
|
255
|
-
import {
|
|
256
|
-
|
|
264
|
+
import {
|
|
265
|
+
type CsrfHeaderNameOption,
|
|
266
|
+
getCurrentCsrfToken,
|
|
267
|
+
handleAuthResponse,
|
|
268
|
+
resolveCsrfHeaderName,
|
|
269
|
+
wipeCurrentCsrfToken,
|
|
270
|
+
} from 'auth-vir';
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The CSRF header prefix for this app. Either `csrfHeaderPrefix` or `csrfHeaderName` must be
|
|
274
|
+
* provided to all CSRF-related functions.
|
|
275
|
+
*/
|
|
276
|
+
const csrfOption: CsrfHeaderNameOption = {
|
|
277
|
+
csrfHeaderPrefix: 'my-app',
|
|
278
|
+
};
|
|
257
279
|
|
|
258
280
|
/** Call this when the user logs in for the first time this session. */
|
|
259
281
|
export async function sendLoginRequest(
|
|
260
282
|
userLoginData: {username: string; password: string},
|
|
261
283
|
loginUrl: string,
|
|
262
284
|
) {
|
|
263
|
-
if (getCurrentCsrfToken().csrfToken) {
|
|
285
|
+
if (getCurrentCsrfToken(csrfOption).csrfToken) {
|
|
264
286
|
throw new Error('Already logged in.');
|
|
265
287
|
}
|
|
266
288
|
|
|
@@ -270,7 +292,7 @@ export async function sendLoginRequest(
|
|
|
270
292
|
credentials: 'include',
|
|
271
293
|
});
|
|
272
294
|
|
|
273
|
-
handleAuthResponse(response);
|
|
295
|
+
handleAuthResponse(response, csrfOption);
|
|
274
296
|
|
|
275
297
|
return response;
|
|
276
298
|
}
|
|
@@ -281,7 +303,7 @@ export async function sendAuthenticatedRequest(
|
|
|
281
303
|
requestInit: Omit<RequestInit, 'headers'> = {},
|
|
282
304
|
headers: Record<string, string> = {},
|
|
283
305
|
) {
|
|
284
|
-
const {csrfToken} = getCurrentCsrfToken();
|
|
306
|
+
const {csrfToken} = getCurrentCsrfToken(csrfOption);
|
|
285
307
|
|
|
286
308
|
if (!csrfToken) {
|
|
287
309
|
throw new Error('Not authenticated.');
|
|
@@ -292,7 +314,7 @@ export async function sendAuthenticatedRequest(
|
|
|
292
314
|
credentials: 'include',
|
|
293
315
|
headers: {
|
|
294
316
|
...headers,
|
|
295
|
-
[
|
|
317
|
+
[resolveCsrfHeaderName(csrfOption)]: csrfToken.token,
|
|
296
318
|
},
|
|
297
319
|
});
|
|
298
320
|
|
|
@@ -302,7 +324,7 @@ export async function sendAuthenticatedRequest(
|
|
|
302
324
|
* another tab.)
|
|
303
325
|
*/
|
|
304
326
|
if (response.status === HttpStatus.Unauthorized) {
|
|
305
|
-
wipeCurrentCsrfToken();
|
|
327
|
+
wipeCurrentCsrfToken(csrfOption);
|
|
306
328
|
throw new Error(`User no longer logged in.`);
|
|
307
329
|
} else {
|
|
308
330
|
return response;
|
|
@@ -311,7 +333,7 @@ export async function sendAuthenticatedRequest(
|
|
|
311
333
|
|
|
312
334
|
/** Call this when the user explicitly clicks a "log out" button. */
|
|
313
335
|
export function logout() {
|
|
314
|
-
wipeCurrentCsrfToken();
|
|
336
|
+
wipeCurrentCsrfToken(csrfOption);
|
|
315
337
|
}
|
|
316
338
|
```
|
|
317
339
|
|
|
@@ -4,9 +4,9 @@ import { type IncomingHttpHeaders, type OutgoingHttpHeaders } from 'node:http';
|
|
|
4
4
|
import { type EmptyObject, type RequireExactlyOne, type RequireOneOrNone } from 'type-fest';
|
|
5
5
|
import { type UserIdResult } from '../auth.js';
|
|
6
6
|
import { type CookieParams } from '../cookie.js';
|
|
7
|
-
import {
|
|
7
|
+
import { type CsrfHeaderNameOption } from '../csrf-token.js';
|
|
8
8
|
import { type JwtKeys, type RawJwtKeys } from '../jwt/jwt-keys.js';
|
|
9
|
-
import { type CreateJwtParams } from '../jwt/jwt.js';
|
|
9
|
+
import { type CreateJwtParams, type ParseJwtParams } from '../jwt/jwt.js';
|
|
10
10
|
/**
|
|
11
11
|
* Output from `BackendAuthClient.getSecureUser()`.
|
|
12
12
|
*
|
|
@@ -31,7 +31,8 @@ export type GetUserResult<DatabaseUser extends AnyObject> = {
|
|
|
31
31
|
*
|
|
32
32
|
* @category Internal
|
|
33
33
|
*/
|
|
34
|
-
export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId extends string | number, AssumedUserParams extends JsonCompatibleObject = EmptyObject
|
|
34
|
+
export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId extends string | number, AssumedUserParams extends JsonCompatibleObject = EmptyObject> = Readonly<{
|
|
35
|
+
csrf: Readonly<CsrfHeaderNameOption>;
|
|
35
36
|
/** The origin of your backend that is offering auth cookies. */
|
|
36
37
|
serviceOrigin: string;
|
|
37
38
|
/** Finds the relevant user from your own database. */
|
|
@@ -58,6 +59,11 @@ export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId exten
|
|
|
58
59
|
*/
|
|
59
60
|
isDev: boolean;
|
|
60
61
|
} & PartialWithUndefined<{
|
|
62
|
+
/**
|
|
63
|
+
* Overwrite the header name used for tracking is an admin is assuming the identity of
|
|
64
|
+
* another user.
|
|
65
|
+
*/
|
|
66
|
+
assumedUserHeaderName: string;
|
|
61
67
|
/**
|
|
62
68
|
* Optionally generate a service origin from request headers. The generated origin is used
|
|
63
69
|
* for set-cookie headers.
|
|
@@ -107,30 +113,33 @@ export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId exten
|
|
|
107
113
|
*
|
|
108
114
|
* @default {minutes: 2}
|
|
109
115
|
*/
|
|
110
|
-
|
|
116
|
+
sessionRefreshStartTime: Readonly<AnyDuration>;
|
|
111
117
|
/**
|
|
112
118
|
* The maximum duration a session can last, regardless of activity. After this time, the
|
|
113
119
|
* user will be logged out even if they are actively using the application.
|
|
114
120
|
*
|
|
115
|
-
* @default {
|
|
121
|
+
* @default {days: 1.5}
|
|
116
122
|
*/
|
|
117
123
|
maxSessionDuration: Readonly<AnyDuration>;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Allowed clock skew tolerance for JWT and CSRF token expiration checks. Accounts for
|
|
126
|
+
* differences between server and client clocks.
|
|
127
|
+
*
|
|
128
|
+
* @default {minutes: 5}
|
|
129
|
+
*/
|
|
130
|
+
allowedClockSkew: Readonly<AnyDuration>;
|
|
122
131
|
}>>;
|
|
123
132
|
/**
|
|
124
133
|
* An auth client for creating and validating JWTs embedded in cookies. This should only be used in
|
|
125
134
|
* a backend environment as it accesses native Node packages.
|
|
126
135
|
*
|
|
127
136
|
* @category Auth : Host
|
|
128
|
-
* @category
|
|
137
|
+
* @category Clients
|
|
129
138
|
*/
|
|
130
|
-
export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId extends string | number, AssumedUserParams extends AnyObject = EmptyObject
|
|
131
|
-
protected readonly config: BackendAuthClientConfig<DatabaseUser, UserId, AssumedUserParams
|
|
139
|
+
export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId extends string | number, AssumedUserParams extends AnyObject = EmptyObject> {
|
|
140
|
+
protected readonly config: BackendAuthClientConfig<DatabaseUser, UserId, AssumedUserParams>;
|
|
132
141
|
protected cachedParsedJwtKeys: Record<string, Readonly<JwtKeys>>;
|
|
133
|
-
constructor(config: BackendAuthClientConfig<DatabaseUser, UserId, AssumedUserParams
|
|
142
|
+
constructor(config: BackendAuthClientConfig<DatabaseUser, UserId, AssumedUserParams>);
|
|
134
143
|
/** Get all the parameters used for cookie generation. */
|
|
135
144
|
protected getCookieParams({ isSignUpCookie, requestHeaders, }: {
|
|
136
145
|
/**
|
|
@@ -173,7 +182,7 @@ export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId ex
|
|
|
173
182
|
* Get all the JWT params used when creating the auth cookie, in case you need them for
|
|
174
183
|
* something else too.
|
|
175
184
|
*/
|
|
176
|
-
getJwtParams(): Promise<Readonly<CreateJwtParams
|
|
185
|
+
getJwtParams(): Promise<Readonly<CreateJwtParams> & ParseJwtParams>;
|
|
177
186
|
/** Use these headers to log out the user. */
|
|
178
187
|
createLogoutHeaders(params: Readonly<RequireExactlyOne<{
|
|
179
188
|
allCookies: true;
|
|
@@ -181,7 +190,7 @@ export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId ex
|
|
|
181
190
|
}> & {
|
|
182
191
|
/** Overrides the client's already established `serviceOrigin`. */
|
|
183
192
|
serviceOrigin?: string | undefined;
|
|
184
|
-
}>): Promise<
|
|
193
|
+
}>): Promise<Record<string, string | string[]> & {
|
|
185
194
|
'set-cookie': string[];
|
|
186
195
|
}>;
|
|
187
196
|
/** Use these headers to log a user in. */
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
import { ensureArray, } from '@augment-vir/common';
|
|
2
|
-
import { calculateRelativeDate, createUtcFullDate, getNowInUtcTimezone, isDateAfter,
|
|
2
|
+
import { calculateRelativeDate, createUtcFullDate, getNowInUtcTimezone, isDateAfter, } from 'date-vir';
|
|
3
3
|
import { extractUserIdFromRequestHeaders, generateLogoutHeaders, generateSuccessfulLoginHeaders, insecureExtractUserIdFromCookieAlone, } from '../auth.js';
|
|
4
|
-
import { AuthCookieName } from '../cookie.js';
|
|
4
|
+
import { AuthCookieName, generateAuthCookie } from '../cookie.js';
|
|
5
|
+
import { defaultAllowedClockSkew, resolveCsrfHeaderName, } from '../csrf-token.js';
|
|
5
6
|
import { AuthHeaderName, mergeHeaderValues } from '../headers.js';
|
|
6
7
|
import { parseJwtKeys } from '../jwt/jwt-keys.js';
|
|
7
|
-
import {
|
|
8
|
+
import { isSessionRefreshReady } from './is-session-refresh-ready.js';
|
|
8
9
|
const defaultSessionIdleTimeout = {
|
|
9
10
|
minutes: 20,
|
|
10
11
|
};
|
|
11
|
-
const
|
|
12
|
+
const defaultSessionRefreshStartTime = {
|
|
12
13
|
minutes: 2,
|
|
13
14
|
};
|
|
14
15
|
const defaultMaxSessionDuration = {
|
|
15
|
-
|
|
16
|
+
days: 1.5,
|
|
16
17
|
};
|
|
17
18
|
/**
|
|
18
19
|
* An auth client for creating and validating JWTs embedded in cookies. This should only be used in
|
|
19
20
|
* a backend environment as it accesses native Node packages.
|
|
20
21
|
*
|
|
21
22
|
* @category Auth : Host
|
|
22
|
-
* @category
|
|
23
|
+
* @category Clients
|
|
23
24
|
*/
|
|
24
25
|
export class BackendAuthClient {
|
|
25
26
|
config;
|
|
@@ -58,16 +59,13 @@ export class BackendAuthClient {
|
|
|
58
59
|
/** Creates a `'cookie-set'` header to refresh the user's session cookie. */
|
|
59
60
|
async createCookieRefreshHeaders({ userIdResult, requestHeaders, }) {
|
|
60
61
|
const now = getNowInUtcTimezone();
|
|
61
|
-
|
|
62
|
+
const clockSkew = this.config.allowedClockSkew || defaultAllowedClockSkew;
|
|
63
|
+
/** Double check that the JWT hasn't already expired (with clock skew tolerance). */
|
|
62
64
|
const isExpiredAlready = isDateAfter({
|
|
63
65
|
fullDate: now,
|
|
64
|
-
relativeTo: userIdResult.jwtExpiration,
|
|
66
|
+
relativeTo: calculateRelativeDate(userIdResult.jwtExpiration, clockSkew),
|
|
65
67
|
});
|
|
66
68
|
if (isExpiredAlready) {
|
|
67
|
-
authLog('auth-vir: SESSION EXPIRED - JWT already expired, user will be logged out', {
|
|
68
|
-
userId: userIdResult.userId,
|
|
69
|
-
jwtExpiration: userIdResult.jwtExpiration,
|
|
70
|
-
});
|
|
71
69
|
return undefined;
|
|
72
70
|
}
|
|
73
71
|
/**
|
|
@@ -83,40 +81,34 @@ export class BackendAuthClient {
|
|
|
83
81
|
relativeTo: maxSessionEndDate,
|
|
84
82
|
});
|
|
85
83
|
if (isSessionExpired) {
|
|
86
|
-
authLog('auth-vir: SESSION EXPIRED - max session duration exceeded, user will be logged out', {
|
|
87
|
-
userId: userIdResult.userId,
|
|
88
|
-
sessionStartedAt: userIdResult.sessionStartedAt,
|
|
89
|
-
maxSessionDuration,
|
|
90
|
-
});
|
|
91
84
|
return undefined;
|
|
92
85
|
}
|
|
93
86
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
*
|
|
100
|
-
* X C=======Y=======R Z
|
|
101
|
-
*
|
|
102
|
-
* - C = current time
|
|
103
|
-
* - R = C + refresh threshold
|
|
104
|
-
* - `=` = the time frame in which {@link isRefreshReady} = true.
|
|
105
|
-
* - X = JWT expiration that has already expired (rejected by {@link isExpiredAlready}.
|
|
106
|
-
* - Y = JWT expiration within the refresh threshold: {@link isRefreshReady} = true.
|
|
107
|
-
* - Z = JWT expiration outside the refresh threshold: {@link isRefreshReady} = false.
|
|
108
|
-
*/
|
|
109
|
-
const sessionRefreshTimeout = this.config.sessionRefreshTimeout || defaultSessionRefreshTimeout;
|
|
110
|
-
const isRefreshReady = isDateAfter({
|
|
111
|
-
fullDate: now,
|
|
112
|
-
relativeTo: calculateRelativeDate(userIdResult.jwtExpiration, negateDuration(sessionRefreshTimeout)),
|
|
87
|
+
const sessionRefreshStartTime = this.config.sessionRefreshStartTime || defaultSessionRefreshStartTime;
|
|
88
|
+
const isRefreshReady = isSessionRefreshReady({
|
|
89
|
+
now,
|
|
90
|
+
jwtIssuedAt: userIdResult.jwtIssuedAt,
|
|
91
|
+
sessionRefreshStartTime,
|
|
113
92
|
});
|
|
114
93
|
if (isRefreshReady) {
|
|
115
|
-
|
|
94
|
+
const isSignUpCookie = userIdResult.cookieName === AuthCookieName.SignUp;
|
|
95
|
+
const cookieParams = await this.getCookieParams({
|
|
96
|
+
isSignUpCookie,
|
|
116
97
|
requestHeaders,
|
|
117
|
-
userId: userIdResult.userId,
|
|
118
|
-
isSignUpCookie: userIdResult.cookieName === AuthCookieName.SignUp,
|
|
119
98
|
});
|
|
99
|
+
const csrfHeaderName = resolveCsrfHeaderName(this.config.csrf);
|
|
100
|
+
const { cookie, expiration } = await generateAuthCookie({
|
|
101
|
+
csrfToken: userIdResult.csrfToken,
|
|
102
|
+
userId: userIdResult.userId,
|
|
103
|
+
sessionStartedAt: userIdResult.sessionStartedAt || Date.now(),
|
|
104
|
+
}, cookieParams);
|
|
105
|
+
return {
|
|
106
|
+
'set-cookie': cookie,
|
|
107
|
+
[csrfHeaderName]: JSON.stringify({
|
|
108
|
+
token: userIdResult.csrfToken,
|
|
109
|
+
expiration,
|
|
110
|
+
}),
|
|
111
|
+
};
|
|
120
112
|
}
|
|
121
113
|
else {
|
|
122
114
|
return undefined;
|
|
@@ -127,7 +119,7 @@ export class BackendAuthClient {
|
|
|
127
119
|
if (!this.config.assumeUser || !(await this.config.assumeUser.canAssumeUser(user))) {
|
|
128
120
|
return undefined;
|
|
129
121
|
}
|
|
130
|
-
const assumedUserHeader = ensureArray(headers[this.config.
|
|
122
|
+
const assumedUserHeader = ensureArray(headers[this.config.assumedUserHeaderName || AuthHeaderName.AssumedUser])[0];
|
|
131
123
|
if (!assumedUserHeader) {
|
|
132
124
|
return undefined;
|
|
133
125
|
}
|
|
@@ -144,11 +136,8 @@ export class BackendAuthClient {
|
|
|
144
136
|
}
|
|
145
137
|
/** Securely extract a user from their request headers. */
|
|
146
138
|
async getSecureUser({ requestHeaders, isSignUpCookie, allowUserAuthRefresh, }) {
|
|
147
|
-
const userIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth
|
|
139
|
+
const userIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), this.config.csrf, isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth);
|
|
148
140
|
if (!userIdResult) {
|
|
149
|
-
if (!isSignUpCookie) {
|
|
150
|
-
authLog('auth-vir: getSecureUser failed - could not extract user from request');
|
|
151
|
-
}
|
|
152
141
|
return undefined;
|
|
153
142
|
}
|
|
154
143
|
const user = await this.getDatabaseUser({
|
|
@@ -157,9 +146,6 @@ export class BackendAuthClient {
|
|
|
157
146
|
isSignUpCookie,
|
|
158
147
|
});
|
|
159
148
|
if (!user) {
|
|
160
|
-
authLog('auth-vir: getSecureUser failed - user not found in database', {
|
|
161
|
-
userId: userIdResult.userId,
|
|
162
|
-
});
|
|
163
149
|
return undefined;
|
|
164
150
|
}
|
|
165
151
|
const assumedUser = await this.getAssumedUser({
|
|
@@ -184,7 +170,7 @@ export class BackendAuthClient {
|
|
|
184
170
|
const rawJwtKeys = await this.config.getJwtKeys();
|
|
185
171
|
const cacheKey = JSON.stringify(rawJwtKeys);
|
|
186
172
|
const cachedParsedKeys = this.cachedParsedJwtKeys[cacheKey];
|
|
187
|
-
const parsedKeys = cachedParsedKeys
|
|
173
|
+
const parsedKeys = cachedParsedKeys || (await parseJwtKeys(rawJwtKeys));
|
|
188
174
|
if (!cachedParsedKeys) {
|
|
189
175
|
this.cachedParsedJwtKeys = { [cacheKey]: parsedKeys };
|
|
190
176
|
}
|
|
@@ -193,25 +179,22 @@ export class BackendAuthClient {
|
|
|
193
179
|
audience: 'server-context',
|
|
194
180
|
issuer: 'server-auth',
|
|
195
181
|
jwtDuration: this.config.userSessionIdleTimeout || defaultSessionIdleTimeout,
|
|
182
|
+
allowedClockSkew: this.config.allowedClockSkew || defaultAllowedClockSkew,
|
|
196
183
|
};
|
|
197
184
|
}
|
|
198
185
|
/** Use these headers to log out the user. */
|
|
199
186
|
async createLogoutHeaders(params) {
|
|
200
|
-
authLog('auth-vir: LOGOUT - BackendAuthClient.createLogoutHeaders called', {
|
|
201
|
-
allCookies: 'allCookies' in params ? params.allCookies : undefined,
|
|
202
|
-
isSignUpCookie: 'isSignUpCookie' in params ? params.isSignUpCookie : undefined,
|
|
203
|
-
}, new Error().stack);
|
|
204
187
|
const signUpCookieHeaders = params.allCookies || params.isSignUpCookie
|
|
205
188
|
? generateLogoutHeaders(await this.getCookieParams({
|
|
206
189
|
isSignUpCookie: true,
|
|
207
190
|
requestHeaders: undefined,
|
|
208
|
-
}), this.config.
|
|
191
|
+
}), this.config.csrf)
|
|
209
192
|
: undefined;
|
|
210
193
|
const authCookieHeaders = params.allCookies || !params.isSignUpCookie
|
|
211
194
|
? generateLogoutHeaders(await this.getCookieParams({
|
|
212
195
|
isSignUpCookie: false,
|
|
213
196
|
requestHeaders: undefined,
|
|
214
|
-
}), this.config.
|
|
197
|
+
}), this.config.csrf)
|
|
215
198
|
: undefined;
|
|
216
199
|
const setCookieHeader = {
|
|
217
200
|
'set-cookie': mergeHeaderValues(signUpCookieHeaders?.['set-cookie'], authCookieHeaders?.['set-cookie']),
|
|
@@ -233,14 +216,14 @@ export class BackendAuthClient {
|
|
|
233
216
|
? generateLogoutHeaders(await this.getCookieParams({
|
|
234
217
|
isSignUpCookie: !isSignUpCookie,
|
|
235
218
|
requestHeaders,
|
|
236
|
-
}), this.config.
|
|
219
|
+
}), this.config.csrf)
|
|
237
220
|
: undefined;
|
|
238
|
-
const existingUserIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth
|
|
221
|
+
const existingUserIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), this.config.csrf, isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth);
|
|
239
222
|
const sessionStartedAt = existingUserIdResult?.sessionStartedAt;
|
|
240
223
|
const newCookieHeaders = await generateSuccessfulLoginHeaders(userId, await this.getCookieParams({
|
|
241
224
|
isSignUpCookie,
|
|
242
225
|
requestHeaders,
|
|
243
|
-
}), this.config.
|
|
226
|
+
}), this.config.csrf, sessionStartedAt);
|
|
244
227
|
return {
|
|
245
228
|
...newCookieHeaders,
|
|
246
229
|
'set-cookie': mergeHeaderValues(newCookieHeaders['set-cookie'], discardOppositeCookieHeaders?.['set-cookie']),
|
|
@@ -270,7 +253,6 @@ export class BackendAuthClient {
|
|
|
270
253
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
271
254
|
const userIdResult = await insecureExtractUserIdFromCookieAlone(requestHeaders, await this.getJwtParams(), AuthCookieName.Auth);
|
|
272
255
|
if (!userIdResult) {
|
|
273
|
-
authLog('auth-vir: getInsecureUser failed - could not extract user from request');
|
|
274
256
|
return undefined;
|
|
275
257
|
}
|
|
276
258
|
const user = await this.getDatabaseUser({
|
|
@@ -279,9 +261,6 @@ export class BackendAuthClient {
|
|
|
279
261
|
assumingUser: undefined,
|
|
280
262
|
});
|
|
281
263
|
if (!user) {
|
|
282
|
-
authLog('auth-vir: getInsecureUser failed - user not found in database', {
|
|
283
|
-
userId: userIdResult.userId,
|
|
284
|
-
});
|
|
285
264
|
return undefined;
|
|
286
265
|
}
|
|
287
266
|
const refreshHeaders = allowUserAuthRefresh &&
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { type createBlockingInterval, type JsonCompatibleObject, type MaybePromise, type PartialWithUndefined, type SelectFrom } from '@augment-vir/common';
|
|
2
2
|
import { type AnyDuration } from 'date-vir';
|
|
3
3
|
import { type EmptyObject } from 'type-fest';
|
|
4
|
+
import { type CsrfHeaderNameOption } from '../csrf-token.js';
|
|
4
5
|
/**
|
|
5
6
|
* Config for {@link FrontendAuthClient}.
|
|
6
7
|
*
|
|
7
8
|
* @category Internal
|
|
8
9
|
*/
|
|
9
|
-
export type FrontendAuthClientConfig =
|
|
10
|
+
export type FrontendAuthClientConfig = Readonly<{
|
|
11
|
+
csrf: Readonly<CsrfHeaderNameOption>;
|
|
12
|
+
}> & PartialWithUndefined<{
|
|
10
13
|
/**
|
|
11
14
|
* Determine if the current user can assume the identity of another user. If this is not
|
|
12
15
|
* defined, all users will be blocked from assuming other user identities.
|
|
@@ -15,8 +18,8 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
|
|
|
15
18
|
/** Called whenever the current user becomes unauthorized and their CSRF token is wiped. */
|
|
16
19
|
authClearedCallback: () => MaybePromise<void>;
|
|
17
20
|
/**
|
|
18
|
-
* Performs automatic checks on an interval to see if the user is still authenticated. Omit
|
|
19
|
-
* to turn off automatic checks.
|
|
21
|
+
* Performs automatic checks on an interval to see if the user is still authenticated. Omit
|
|
22
|
+
* this to turn off automatic checks.
|
|
20
23
|
*/
|
|
21
24
|
checkUser: {
|
|
22
25
|
/**
|
|
@@ -27,8 +30,8 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
|
|
|
27
30
|
* If the user is not currently authorized, this should return `undefined` to prevent
|
|
28
31
|
* unnecessary network traffic.
|
|
29
32
|
*
|
|
30
|
-
* This will be called any time the user interacts with the page, debounced by the
|
|
31
|
-
* `debounce` property.
|
|
33
|
+
* This will be called any time the user interacts with the page, debounced by the
|
|
34
|
+
* adjacent `debounce` property.
|
|
32
35
|
*/
|
|
33
36
|
performCheck: () => MaybePromise<SelectFrom<Response, {
|
|
34
37
|
status: true;
|
|
@@ -40,10 +43,20 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
|
|
|
40
43
|
*/
|
|
41
44
|
debounce?: AnyDuration | undefined;
|
|
42
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* Overwrite the header name used for tracking is an admin is assuming the identity of
|
|
48
|
+
* another user.
|
|
49
|
+
*/
|
|
50
|
+
assumedUserHeaderName: string;
|
|
51
|
+
/**
|
|
52
|
+
* Allowed clock skew tolerance for CSRF token expiration checks. Accounts for differences
|
|
53
|
+
* between server and client clocks.
|
|
54
|
+
*
|
|
55
|
+
* @default {minutes: 5}
|
|
56
|
+
*/
|
|
57
|
+
allowedClockSkew: Readonly<AnyDuration>;
|
|
43
58
|
overrides: PartialWithUndefined<{
|
|
44
59
|
localStorage: Pick<Storage, 'setItem' | 'removeItem' | 'getItem'>;
|
|
45
|
-
csrfHeaderName: string;
|
|
46
|
-
assumedUserHeaderName: string;
|
|
47
60
|
}>;
|
|
48
61
|
}>;
|
|
49
62
|
/**
|
|
@@ -51,21 +64,21 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
|
|
|
51
64
|
* in a frontend environment as it accesses native browser APIs.
|
|
52
65
|
*
|
|
53
66
|
* @category Auth : Client
|
|
54
|
-
* @category
|
|
67
|
+
* @category Clients
|
|
55
68
|
*/
|
|
56
69
|
export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject = EmptyObject> {
|
|
57
70
|
protected readonly config: FrontendAuthClientConfig;
|
|
58
71
|
protected userCheckInterval: undefined | ReturnType<typeof createBlockingInterval>;
|
|
59
72
|
/** Used to clean up the activity listener on `.destroy()`. */
|
|
60
73
|
protected removeActivityListener: VoidFunction | undefined;
|
|
61
|
-
constructor(config
|
|
74
|
+
constructor(config: FrontendAuthClientConfig);
|
|
62
75
|
/**
|
|
63
76
|
* Destroys the client and performs all necessary cleanup (like clearing the user check
|
|
64
77
|
* interval).
|
|
65
78
|
*/
|
|
66
79
|
destroy(): void;
|
|
67
80
|
/** Wraps {@link getCurrentCsrfToken} to automatically handle wiping an invalid CSRF token. */
|
|
68
|
-
getCurrentCsrfToken():
|
|
81
|
+
getCurrentCsrfToken(): string | undefined;
|
|
69
82
|
/**
|
|
70
83
|
* Assume the given user. Pass `undefined` to wipe the currently assumed user.
|
|
71
84
|
*
|
|
@@ -80,7 +93,7 @@ export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatible
|
|
|
80
93
|
* `@augment-vir/common`](https://electrovir.github.io/augment-vir/functions/mergeDeep.html) to
|
|
81
94
|
* combine them with these.
|
|
82
95
|
*/
|
|
83
|
-
createAuthenticatedRequestInit():
|
|
96
|
+
createAuthenticatedRequestInit(): RequestInit;
|
|
84
97
|
/** Wipes the current user auth. */
|
|
85
98
|
logout(): Promise<void>;
|
|
86
99
|
/**
|