auth-vir 2.7.2 → 3.0.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 +33 -11
- package/dist/auth-client/backend-auth.client.d.ts +29 -18
- package/dist/auth-client/backend-auth.client.js +47 -64
- 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 +84 -97
- 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. */
|
|
@@ -45,6 +46,7 @@ export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId exten
|
|
|
45
46
|
* their user identity. Otherwise, this is `undefined`.
|
|
46
47
|
*/
|
|
47
48
|
assumingUser: AssumedUserParams | undefined;
|
|
49
|
+
requestHeaders: Readonly<IncomingHttpHeaders>;
|
|
48
50
|
}) => MaybePromise<DatabaseUser | undefined | null>;
|
|
49
51
|
/**
|
|
50
52
|
* Get JWT keys produced by {@link generateNewJwtKeys}. Make sure that each time this is
|
|
@@ -58,6 +60,11 @@ export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId exten
|
|
|
58
60
|
*/
|
|
59
61
|
isDev: boolean;
|
|
60
62
|
} & PartialWithUndefined<{
|
|
63
|
+
/**
|
|
64
|
+
* Overwrite the header name used for tracking is an admin is assuming the identity of
|
|
65
|
+
* another user.
|
|
66
|
+
*/
|
|
67
|
+
assumedUserHeaderName: string;
|
|
61
68
|
/**
|
|
62
69
|
* Optionally generate a service origin from request headers. The generated origin is used
|
|
63
70
|
* for set-cookie headers.
|
|
@@ -107,30 +114,33 @@ export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId exten
|
|
|
107
114
|
*
|
|
108
115
|
* @default {minutes: 2}
|
|
109
116
|
*/
|
|
110
|
-
|
|
117
|
+
sessionRefreshStartTime: Readonly<AnyDuration>;
|
|
111
118
|
/**
|
|
112
119
|
* The maximum duration a session can last, regardless of activity. After this time, the
|
|
113
120
|
* user will be logged out even if they are actively using the application.
|
|
114
121
|
*
|
|
115
|
-
* @default {
|
|
122
|
+
* @default {days: 1.5}
|
|
116
123
|
*/
|
|
117
124
|
maxSessionDuration: Readonly<AnyDuration>;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Allowed clock skew tolerance for JWT and CSRF token expiration checks. Accounts for
|
|
127
|
+
* differences between server and client clocks.
|
|
128
|
+
*
|
|
129
|
+
* @default {minutes: 5}
|
|
130
|
+
*/
|
|
131
|
+
allowedClockSkew: Readonly<AnyDuration>;
|
|
122
132
|
}>>;
|
|
123
133
|
/**
|
|
124
134
|
* An auth client for creating and validating JWTs embedded in cookies. This should only be used in
|
|
125
135
|
* a backend environment as it accesses native Node packages.
|
|
126
136
|
*
|
|
127
137
|
* @category Auth : Host
|
|
128
|
-
* @category
|
|
138
|
+
* @category Clients
|
|
129
139
|
*/
|
|
130
|
-
export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId extends string | number, AssumedUserParams extends AnyObject = EmptyObject
|
|
131
|
-
protected readonly config: BackendAuthClientConfig<DatabaseUser, UserId, AssumedUserParams
|
|
140
|
+
export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId extends string | number, AssumedUserParams extends AnyObject = EmptyObject> {
|
|
141
|
+
protected readonly config: BackendAuthClientConfig<DatabaseUser, UserId, AssumedUserParams>;
|
|
132
142
|
protected cachedParsedJwtKeys: Record<string, Readonly<JwtKeys>>;
|
|
133
|
-
constructor(config: BackendAuthClientConfig<DatabaseUser, UserId, AssumedUserParams
|
|
143
|
+
constructor(config: BackendAuthClientConfig<DatabaseUser, UserId, AssumedUserParams>);
|
|
134
144
|
/** Get all the parameters used for cookie generation. */
|
|
135
145
|
protected getCookieParams({ isSignUpCookie, requestHeaders, }: {
|
|
136
146
|
/**
|
|
@@ -143,10 +153,11 @@ export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId ex
|
|
|
143
153
|
requestHeaders: Readonly<IncomingHttpHeaders> | undefined;
|
|
144
154
|
}): Promise<Readonly<CookieParams>>;
|
|
145
155
|
/** Calls the provided `getUserFromDatabase` config. */
|
|
146
|
-
protected getDatabaseUser({ isSignUpCookie, userId, assumingUser, }: {
|
|
156
|
+
protected getDatabaseUser({ isSignUpCookie, userId, assumingUser, requestHeaders, }: {
|
|
147
157
|
userId: UserId | undefined;
|
|
148
158
|
assumingUser: AssumedUserParams | undefined;
|
|
149
159
|
isSignUpCookie: boolean;
|
|
160
|
+
requestHeaders: IncomingHttpHeaders;
|
|
150
161
|
}): Promise<undefined | DatabaseUser>;
|
|
151
162
|
/** Creates a `'cookie-set'` header to refresh the user's session cookie. */
|
|
152
163
|
protected createCookieRefreshHeaders({ userIdResult, requestHeaders, }: {
|
|
@@ -154,9 +165,9 @@ export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId ex
|
|
|
154
165
|
requestHeaders: IncomingHttpHeaders;
|
|
155
166
|
}): Promise<OutgoingHttpHeaders | undefined>;
|
|
156
167
|
/** Reads the user's assumed user headers and, if configured, gets the assumed user. */
|
|
157
|
-
protected getAssumedUser({
|
|
168
|
+
protected getAssumedUser({ requestHeaders, user, }: {
|
|
158
169
|
user: DatabaseUser;
|
|
159
|
-
|
|
170
|
+
requestHeaders: IncomingHttpHeaders;
|
|
160
171
|
}): Promise<DatabaseUser | undefined>;
|
|
161
172
|
/** Securely extract a user from their request headers. */
|
|
162
173
|
getSecureUser({ requestHeaders, isSignUpCookie, allowUserAuthRefresh, }: {
|
|
@@ -173,7 +184,7 @@ export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId ex
|
|
|
173
184
|
* Get all the JWT params used when creating the auth cookie, in case you need them for
|
|
174
185
|
* something else too.
|
|
175
186
|
*/
|
|
176
|
-
getJwtParams(): Promise<Readonly<CreateJwtParams
|
|
187
|
+
getJwtParams(): Promise<Readonly<CreateJwtParams> & ParseJwtParams>;
|
|
177
188
|
/** Use these headers to log out the user. */
|
|
178
189
|
createLogoutHeaders(params: Readonly<RequireExactlyOne<{
|
|
179
190
|
allCookies: true;
|
|
@@ -181,7 +192,7 @@ export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId ex
|
|
|
181
192
|
}> & {
|
|
182
193
|
/** Overrides the client's already established `serviceOrigin`. */
|
|
183
194
|
serviceOrigin?: string | undefined;
|
|
184
|
-
}>): Promise<
|
|
195
|
+
}>): Promise<Record<string, string | string[]> & {
|
|
185
196
|
'set-cookie': string[];
|
|
186
197
|
}>;
|
|
187
198
|
/** 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;
|
|
@@ -41,7 +42,7 @@ export class BackendAuthClient {
|
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
/** Calls the provided `getUserFromDatabase` config. */
|
|
44
|
-
async getDatabaseUser({ isSignUpCookie, userId, assumingUser, }) {
|
|
45
|
+
async getDatabaseUser({ isSignUpCookie, userId, assumingUser, requestHeaders, }) {
|
|
45
46
|
if (!userId) {
|
|
46
47
|
return undefined;
|
|
47
48
|
}
|
|
@@ -49,6 +50,7 @@ export class BackendAuthClient {
|
|
|
49
50
|
assumingUser,
|
|
50
51
|
userId,
|
|
51
52
|
isSignUpCookie,
|
|
53
|
+
requestHeaders,
|
|
52
54
|
});
|
|
53
55
|
if (!authenticatedUser) {
|
|
54
56
|
return undefined;
|
|
@@ -58,16 +60,13 @@ export class BackendAuthClient {
|
|
|
58
60
|
/** Creates a `'cookie-set'` header to refresh the user's session cookie. */
|
|
59
61
|
async createCookieRefreshHeaders({ userIdResult, requestHeaders, }) {
|
|
60
62
|
const now = getNowInUtcTimezone();
|
|
61
|
-
|
|
63
|
+
const clockSkew = this.config.allowedClockSkew || defaultAllowedClockSkew;
|
|
64
|
+
/** Double check that the JWT hasn't already expired (with clock skew tolerance). */
|
|
62
65
|
const isExpiredAlready = isDateAfter({
|
|
63
66
|
fullDate: now,
|
|
64
|
-
relativeTo: userIdResult.jwtExpiration,
|
|
67
|
+
relativeTo: calculateRelativeDate(userIdResult.jwtExpiration, clockSkew),
|
|
65
68
|
});
|
|
66
69
|
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
70
|
return undefined;
|
|
72
71
|
}
|
|
73
72
|
/**
|
|
@@ -83,51 +82,45 @@ export class BackendAuthClient {
|
|
|
83
82
|
relativeTo: maxSessionEndDate,
|
|
84
83
|
});
|
|
85
84
|
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
85
|
return undefined;
|
|
92
86
|
}
|
|
93
87
|
}
|
|
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)),
|
|
88
|
+
const sessionRefreshStartTime = this.config.sessionRefreshStartTime || defaultSessionRefreshStartTime;
|
|
89
|
+
const isRefreshReady = isSessionRefreshReady({
|
|
90
|
+
now,
|
|
91
|
+
jwtIssuedAt: userIdResult.jwtIssuedAt,
|
|
92
|
+
sessionRefreshStartTime,
|
|
113
93
|
});
|
|
114
94
|
if (isRefreshReady) {
|
|
115
|
-
|
|
95
|
+
const isSignUpCookie = userIdResult.cookieName === AuthCookieName.SignUp;
|
|
96
|
+
const cookieParams = await this.getCookieParams({
|
|
97
|
+
isSignUpCookie,
|
|
116
98
|
requestHeaders,
|
|
117
|
-
userId: userIdResult.userId,
|
|
118
|
-
isSignUpCookie: userIdResult.cookieName === AuthCookieName.SignUp,
|
|
119
99
|
});
|
|
100
|
+
const csrfHeaderName = resolveCsrfHeaderName(this.config.csrf);
|
|
101
|
+
const { cookie, expiration } = await generateAuthCookie({
|
|
102
|
+
csrfToken: userIdResult.csrfToken,
|
|
103
|
+
userId: userIdResult.userId,
|
|
104
|
+
sessionStartedAt: userIdResult.sessionStartedAt || Date.now(),
|
|
105
|
+
}, cookieParams);
|
|
106
|
+
return {
|
|
107
|
+
'set-cookie': cookie,
|
|
108
|
+
[csrfHeaderName]: JSON.stringify({
|
|
109
|
+
token: userIdResult.csrfToken,
|
|
110
|
+
expiration,
|
|
111
|
+
}),
|
|
112
|
+
};
|
|
120
113
|
}
|
|
121
114
|
else {
|
|
122
115
|
return undefined;
|
|
123
116
|
}
|
|
124
117
|
}
|
|
125
118
|
/** Reads the user's assumed user headers and, if configured, gets the assumed user. */
|
|
126
|
-
async getAssumedUser({
|
|
119
|
+
async getAssumedUser({ requestHeaders, user, }) {
|
|
127
120
|
if (!this.config.assumeUser || !(await this.config.assumeUser.canAssumeUser(user))) {
|
|
128
121
|
return undefined;
|
|
129
122
|
}
|
|
130
|
-
const assumedUserHeader = ensureArray(
|
|
123
|
+
const assumedUserHeader = ensureArray(requestHeaders[this.config.assumedUserHeaderName || AuthHeaderName.AssumedUser])[0];
|
|
131
124
|
if (!assumedUserHeader) {
|
|
132
125
|
return undefined;
|
|
133
126
|
}
|
|
@@ -139,31 +132,27 @@ export class BackendAuthClient {
|
|
|
139
132
|
isSignUpCookie: false,
|
|
140
133
|
userId: parsedAssumedUserData.userId,
|
|
141
134
|
assumingUser: parsedAssumedUserData.assumedUserParams,
|
|
135
|
+
requestHeaders,
|
|
142
136
|
});
|
|
143
137
|
return assumedUser;
|
|
144
138
|
}
|
|
145
139
|
/** Securely extract a user from their request headers. */
|
|
146
140
|
async getSecureUser({ requestHeaders, isSignUpCookie, allowUserAuthRefresh, }) {
|
|
147
|
-
const userIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth
|
|
141
|
+
const userIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), this.config.csrf, isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth);
|
|
148
142
|
if (!userIdResult) {
|
|
149
|
-
if (!isSignUpCookie) {
|
|
150
|
-
authLog('auth-vir: getSecureUser failed - could not extract user from request');
|
|
151
|
-
}
|
|
152
143
|
return undefined;
|
|
153
144
|
}
|
|
154
145
|
const user = await this.getDatabaseUser({
|
|
155
146
|
userId: userIdResult.userId,
|
|
156
147
|
assumingUser: undefined,
|
|
157
148
|
isSignUpCookie,
|
|
149
|
+
requestHeaders,
|
|
158
150
|
});
|
|
159
151
|
if (!user) {
|
|
160
|
-
authLog('auth-vir: getSecureUser failed - user not found in database', {
|
|
161
|
-
userId: userIdResult.userId,
|
|
162
|
-
});
|
|
163
152
|
return undefined;
|
|
164
153
|
}
|
|
165
154
|
const assumedUser = await this.getAssumedUser({
|
|
166
|
-
|
|
155
|
+
requestHeaders,
|
|
167
156
|
user,
|
|
168
157
|
});
|
|
169
158
|
const cookieRefreshHeaders = (await this.createCookieRefreshHeaders({
|
|
@@ -184,7 +173,7 @@ export class BackendAuthClient {
|
|
|
184
173
|
const rawJwtKeys = await this.config.getJwtKeys();
|
|
185
174
|
const cacheKey = JSON.stringify(rawJwtKeys);
|
|
186
175
|
const cachedParsedKeys = this.cachedParsedJwtKeys[cacheKey];
|
|
187
|
-
const parsedKeys = cachedParsedKeys
|
|
176
|
+
const parsedKeys = cachedParsedKeys || (await parseJwtKeys(rawJwtKeys));
|
|
188
177
|
if (!cachedParsedKeys) {
|
|
189
178
|
this.cachedParsedJwtKeys = { [cacheKey]: parsedKeys };
|
|
190
179
|
}
|
|
@@ -193,25 +182,22 @@ export class BackendAuthClient {
|
|
|
193
182
|
audience: 'server-context',
|
|
194
183
|
issuer: 'server-auth',
|
|
195
184
|
jwtDuration: this.config.userSessionIdleTimeout || defaultSessionIdleTimeout,
|
|
185
|
+
allowedClockSkew: this.config.allowedClockSkew || defaultAllowedClockSkew,
|
|
196
186
|
};
|
|
197
187
|
}
|
|
198
188
|
/** Use these headers to log out the user. */
|
|
199
189
|
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
190
|
const signUpCookieHeaders = params.allCookies || params.isSignUpCookie
|
|
205
191
|
? generateLogoutHeaders(await this.getCookieParams({
|
|
206
192
|
isSignUpCookie: true,
|
|
207
193
|
requestHeaders: undefined,
|
|
208
|
-
}), this.config.
|
|
194
|
+
}), this.config.csrf)
|
|
209
195
|
: undefined;
|
|
210
196
|
const authCookieHeaders = params.allCookies || !params.isSignUpCookie
|
|
211
197
|
? generateLogoutHeaders(await this.getCookieParams({
|
|
212
198
|
isSignUpCookie: false,
|
|
213
199
|
requestHeaders: undefined,
|
|
214
|
-
}), this.config.
|
|
200
|
+
}), this.config.csrf)
|
|
215
201
|
: undefined;
|
|
216
202
|
const setCookieHeader = {
|
|
217
203
|
'set-cookie': mergeHeaderValues(signUpCookieHeaders?.['set-cookie'], authCookieHeaders?.['set-cookie']),
|
|
@@ -233,14 +219,14 @@ export class BackendAuthClient {
|
|
|
233
219
|
? generateLogoutHeaders(await this.getCookieParams({
|
|
234
220
|
isSignUpCookie: !isSignUpCookie,
|
|
235
221
|
requestHeaders,
|
|
236
|
-
}), this.config.
|
|
222
|
+
}), this.config.csrf)
|
|
237
223
|
: undefined;
|
|
238
|
-
const existingUserIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth
|
|
224
|
+
const existingUserIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), this.config.csrf, isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth);
|
|
239
225
|
const sessionStartedAt = existingUserIdResult?.sessionStartedAt;
|
|
240
226
|
const newCookieHeaders = await generateSuccessfulLoginHeaders(userId, await this.getCookieParams({
|
|
241
227
|
isSignUpCookie,
|
|
242
228
|
requestHeaders,
|
|
243
|
-
}), this.config.
|
|
229
|
+
}), this.config.csrf, sessionStartedAt);
|
|
244
230
|
return {
|
|
245
231
|
...newCookieHeaders,
|
|
246
232
|
'set-cookie': mergeHeaderValues(newCookieHeaders['set-cookie'], discardOppositeCookieHeaders?.['set-cookie']),
|
|
@@ -270,18 +256,15 @@ export class BackendAuthClient {
|
|
|
270
256
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
271
257
|
const userIdResult = await insecureExtractUserIdFromCookieAlone(requestHeaders, await this.getJwtParams(), AuthCookieName.Auth);
|
|
272
258
|
if (!userIdResult) {
|
|
273
|
-
authLog('auth-vir: getInsecureUser failed - could not extract user from request');
|
|
274
259
|
return undefined;
|
|
275
260
|
}
|
|
276
261
|
const user = await this.getDatabaseUser({
|
|
277
262
|
isSignUpCookie: false,
|
|
278
263
|
userId: userIdResult.userId,
|
|
279
264
|
assumingUser: undefined,
|
|
265
|
+
requestHeaders,
|
|
280
266
|
});
|
|
281
267
|
if (!user) {
|
|
282
|
-
authLog('auth-vir: getInsecureUser failed - user not found in database', {
|
|
283
|
-
userId: userIdResult.userId,
|
|
284
|
-
});
|
|
285
268
|
return undefined;
|
|
286
269
|
}
|
|
287
270
|
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
|
/**
|