auth-vir 3.1.2 → 5.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 +38 -25
- package/dist/auth-client/backend-auth.client.d.ts +1 -1
- package/dist/auth-client/backend-auth.client.js +40 -23
- package/dist/auth-client/frontend-auth.client.d.ts +8 -16
- package/dist/auth-client/frontend-auth.client.js +5 -28
- package/dist/auth.d.ts +14 -27
- package/dist/auth.js +21 -41
- package/dist/cookie.d.ts +41 -14
- package/dist/cookie.js +73 -31
- package/dist/csrf-token.d.ts +6 -136
- package/dist/csrf-token.js +19 -118
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -2
- package/dist/jwt/jwt.d.ts +14 -2
- package/dist/jwt/jwt.js +10 -1
- package/package.json +1 -2
- package/src/auth-client/backend-auth.client.ts +45 -30
- package/src/auth-client/frontend-auth.client.ts +6 -55
- package/src/auth.ts +28 -72
- package/src/cookie.ts +99 -48
- package/src/csrf-token.ts +22 -232
- package/src/index.ts +0 -2
- package/src/jwt/jwt.ts +15 -3
- package/dist/csrf-token-store.d.ts +0 -21
- package/dist/csrf-token-store.js +0 -35
- package/dist/mock-csrf-token-store.d.ts +0 -64
- package/dist/mock-csrf-token-store.js +0 -107
- package/src/csrf-token-store.ts +0 -54
- package/src/mock-csrf-token-store.ts +0 -141
package/README.md
CHANGED
|
@@ -73,6 +73,7 @@ Here's a full example of how to use all host / server / backend side auth functi
|
|
|
73
73
|
```TypeScript
|
|
74
74
|
import {type ClientRequest, type ServerResponse} from 'node:http';
|
|
75
75
|
import {
|
|
76
|
+
AuthCookie,
|
|
76
77
|
doesPasswordMatchHash,
|
|
77
78
|
extractUserIdFromRequestHeaders,
|
|
78
79
|
generateNewJwtKeys,
|
|
@@ -114,8 +115,15 @@ export async function handleLogin(
|
|
|
114
115
|
throw new Error('Credentials mismatch.');
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
const authHeaders = await generateSuccessfulLoginHeaders(user.id, cookieParams
|
|
118
|
-
|
|
118
|
+
const authHeaders = await generateSuccessfulLoginHeaders(user.id, cookieParams);
|
|
119
|
+
Object.entries(authHeaders).forEach(
|
|
120
|
+
([
|
|
121
|
+
key,
|
|
122
|
+
value,
|
|
123
|
+
]) => {
|
|
124
|
+
response.setHeader(key, value);
|
|
125
|
+
},
|
|
126
|
+
);
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
/**
|
|
@@ -130,8 +138,15 @@ export async function createUser(
|
|
|
130
138
|
) {
|
|
131
139
|
const newUser = await createUserInDatabase(userRequestData);
|
|
132
140
|
|
|
133
|
-
const authHeaders = await generateSuccessfulLoginHeaders(newUser.id, cookieParams
|
|
134
|
-
|
|
141
|
+
const authHeaders = await generateSuccessfulLoginHeaders(newUser.id, cookieParams);
|
|
142
|
+
Object.entries(authHeaders).forEach(
|
|
143
|
+
([
|
|
144
|
+
key,
|
|
145
|
+
value,
|
|
146
|
+
]) => {
|
|
147
|
+
response.setHeader(key, value);
|
|
148
|
+
},
|
|
149
|
+
);
|
|
135
150
|
}
|
|
136
151
|
|
|
137
152
|
/**
|
|
@@ -141,7 +156,12 @@ export async function createUser(
|
|
|
141
156
|
*/
|
|
142
157
|
export async function getAuthenticatedUser(request: ClientRequest) {
|
|
143
158
|
const userId = (
|
|
144
|
-
await extractUserIdFromRequestHeaders<MyUserId>(
|
|
159
|
+
await extractUserIdFromRequestHeaders<MyUserId>(
|
|
160
|
+
request.getHeaders(),
|
|
161
|
+
jwtParams,
|
|
162
|
+
csrfOption,
|
|
163
|
+
AuthCookie.Auth,
|
|
164
|
+
)
|
|
145
165
|
)?.userId;
|
|
146
166
|
const user = userId ? findUserInDatabaseById(userId) : undefined;
|
|
147
167
|
|
|
@@ -260,13 +280,7 @@ Here's a full example of how to use all the client / frontend side auth function
|
|
|
260
280
|
|
|
261
281
|
```TypeScript
|
|
262
282
|
import {HttpStatus} from '@augment-vir/common';
|
|
263
|
-
import {
|
|
264
|
-
type CsrfHeaderNameOption,
|
|
265
|
-
getCurrentCsrfToken,
|
|
266
|
-
handleAuthResponse,
|
|
267
|
-
resolveCsrfHeaderName,
|
|
268
|
-
wipeCurrentCsrfToken,
|
|
269
|
-
} from 'auth-vir';
|
|
283
|
+
import {type CsrfHeaderNameOption, getCurrentCsrfToken, resolveCsrfHeaderName} from 'auth-vir';
|
|
270
284
|
|
|
271
285
|
/**
|
|
272
286
|
* The CSRF header prefix for this app. Either `csrfHeaderPrefix` or `csrfHeaderName` must be
|
|
@@ -281,7 +295,7 @@ export async function sendLoginRequest(
|
|
|
281
295
|
userLoginData: {username: string; password: string},
|
|
282
296
|
loginUrl: string,
|
|
283
297
|
) {
|
|
284
|
-
if (
|
|
298
|
+
if (getCurrentCsrfToken()) {
|
|
285
299
|
throw new Error('Already logged in.');
|
|
286
300
|
}
|
|
287
301
|
|
|
@@ -291,7 +305,7 @@ export async function sendLoginRequest(
|
|
|
291
305
|
credentials: 'include',
|
|
292
306
|
});
|
|
293
307
|
|
|
294
|
-
|
|
308
|
+
/** The CSRF token cookie is automatically stored by the browser from the Set-Cookie header. */
|
|
295
309
|
|
|
296
310
|
return response;
|
|
297
311
|
}
|
|
@@ -302,7 +316,7 @@ export async function sendAuthenticatedRequest(
|
|
|
302
316
|
requestInit: Omit<RequestInit, 'headers'> = {},
|
|
303
317
|
headers: Record<string, string> = {},
|
|
304
318
|
) {
|
|
305
|
-
const
|
|
319
|
+
const csrfToken = getCurrentCsrfToken();
|
|
306
320
|
|
|
307
321
|
if (!csrfToken) {
|
|
308
322
|
throw new Error('Not authenticated.');
|
|
@@ -313,26 +327,25 @@ export async function sendAuthenticatedRequest(
|
|
|
313
327
|
credentials: 'include',
|
|
314
328
|
headers: {
|
|
315
329
|
...headers,
|
|
316
|
-
[resolveCsrfHeaderName(csrfOption)]: csrfToken
|
|
330
|
+
[resolveCsrfHeaderName(csrfOption)]: csrfToken,
|
|
317
331
|
},
|
|
318
332
|
});
|
|
319
333
|
|
|
320
|
-
/**
|
|
321
|
-
* This indicates the user is no longer authorized and thus needs to login again. (This likely
|
|
322
|
-
* means that their session timed out or they clicked a "log out" button onr your website in
|
|
323
|
-
* another tab.)
|
|
324
|
-
*/
|
|
325
334
|
if (response.status === HttpStatus.Unauthorized) {
|
|
326
|
-
await wipeCurrentCsrfToken(csrfOption);
|
|
327
335
|
throw new Error(`User no longer logged in.`);
|
|
328
336
|
} else {
|
|
329
337
|
return response;
|
|
330
338
|
}
|
|
331
339
|
}
|
|
332
340
|
|
|
333
|
-
/**
|
|
334
|
-
|
|
335
|
-
|
|
341
|
+
/**
|
|
342
|
+
* Call this when the user explicitly clicks a "log out" button. The backend clears the auth and
|
|
343
|
+
* CSRF cookies via Set-Cookie headers.
|
|
344
|
+
*/
|
|
345
|
+
export async function logout(logoutUrl: string) {
|
|
346
|
+
await sendAuthenticatedRequest(logoutUrl, {
|
|
347
|
+
method: 'post',
|
|
348
|
+
});
|
|
336
349
|
}
|
|
337
350
|
```
|
|
338
351
|
|
|
@@ -217,7 +217,7 @@ export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId ex
|
|
|
217
217
|
userId: UserId;
|
|
218
218
|
cookieParams: Readonly<CookieParams>;
|
|
219
219
|
existingUserIdResult: Readonly<UserIdResult<UserId>>;
|
|
220
|
-
}): Promise<Record<string, string>>;
|
|
220
|
+
}): Promise<Record<string, string | string[]>>;
|
|
221
221
|
/** Use these headers to log a user in. */
|
|
222
222
|
createLoginHeaders({ userId, requestHeaders, isSignUpCookie, }: {
|
|
223
223
|
userId: UserId;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ensureArray, } from '@augment-vir/common';
|
|
2
2
|
import { calculateRelativeDate, createUtcFullDate, getNowInUtcTimezone, isDateAfter, } from 'date-vir';
|
|
3
3
|
import { extractUserIdFromRequestHeaders, generateLogoutHeaders, generateSuccessfulLoginHeaders, insecureExtractUserIdFromCookieAlone, } from '../auth.js';
|
|
4
|
-
import {
|
|
5
|
-
import { defaultAllowedClockSkew, resolveCsrfHeaderName, } from '../csrf-token.js';
|
|
4
|
+
import { AuthCookie, generateAuthCookie, generateCsrfCookie } from '../cookie.js';
|
|
6
5
|
import { AuthHeaderName, mergeHeaderValues } from '../headers.js';
|
|
7
6
|
import { parseJwtKeys } from '../jwt/jwt-keys.js';
|
|
7
|
+
import { defaultAllowedClockSkew } from '../jwt/jwt.js';
|
|
8
8
|
import { isSessionRefreshReady } from './is-session-refresh-ready.js';
|
|
9
9
|
const defaultSessionIdleTimeout = {
|
|
10
10
|
minutes: 20,
|
|
@@ -55,7 +55,7 @@ export class BackendAuthClient {
|
|
|
55
55
|
hostOrigin: serviceOrigin || this.config.serviceOrigin,
|
|
56
56
|
jwtParams: await this.getJwtParams(),
|
|
57
57
|
isDev: this.config.isDev,
|
|
58
|
-
|
|
58
|
+
authCookie: isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth,
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
/** Calls the provided `getUserFromDatabase` config. */
|
|
@@ -133,23 +133,22 @@ export class BackendAuthClient {
|
|
|
133
133
|
sessionRefreshStartTime,
|
|
134
134
|
});
|
|
135
135
|
if (isRefreshReady) {
|
|
136
|
-
const isSignUpCookie = userIdResult.cookieName ===
|
|
136
|
+
const isSignUpCookie = userIdResult.cookieName === AuthCookie.SignUp;
|
|
137
137
|
const cookieParams = await this.getCookieParams({
|
|
138
138
|
isSignUpCookie,
|
|
139
139
|
requestHeaders,
|
|
140
140
|
});
|
|
141
|
-
const
|
|
142
|
-
const { cookie, expiration } = await generateAuthCookie({
|
|
141
|
+
const authCookie = await generateAuthCookie({
|
|
143
142
|
csrfToken: userIdResult.csrfToken,
|
|
144
143
|
userId: userIdResult.userId,
|
|
145
144
|
sessionStartedAt: userIdResult.sessionStartedAt || Date.now(),
|
|
146
145
|
}, cookieParams);
|
|
146
|
+
const csrfCookie = generateCsrfCookie(userIdResult.csrfToken, cookieParams);
|
|
147
147
|
return {
|
|
148
|
-
'set-cookie':
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}),
|
|
148
|
+
'set-cookie': [
|
|
149
|
+
authCookie,
|
|
150
|
+
csrfCookie,
|
|
151
|
+
],
|
|
153
152
|
};
|
|
154
153
|
}
|
|
155
154
|
else {
|
|
@@ -187,7 +186,7 @@ export class BackendAuthClient {
|
|
|
187
186
|
}
|
|
188
187
|
/** Securely extract a user from their request headers. */
|
|
189
188
|
async getSecureUser({ requestHeaders, isSignUpCookie, allowUserAuthRefresh, }) {
|
|
190
|
-
const userIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), this.config.csrf, isSignUpCookie ?
|
|
189
|
+
const userIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), this.config.csrf, isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth);
|
|
191
190
|
if (!userIdResult) {
|
|
192
191
|
this.logForUser({
|
|
193
192
|
user: undefined,
|
|
@@ -218,14 +217,28 @@ export class BackendAuthClient {
|
|
|
218
217
|
requestHeaders,
|
|
219
218
|
user,
|
|
220
219
|
});
|
|
221
|
-
const cookieRefreshHeaders =
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
220
|
+
const cookieRefreshHeaders = allowUserAuthRefresh
|
|
221
|
+
? await this.createCookieRefreshHeaders({
|
|
222
|
+
userIdResult,
|
|
223
|
+
requestHeaders,
|
|
224
|
+
})
|
|
225
|
+
: undefined;
|
|
226
|
+
/**
|
|
227
|
+
* Always include the CSRF cookie so it gets re-established if the browser clears it. When
|
|
228
|
+
* session refresh fires, its headers already include a CSRF cookie.
|
|
229
|
+
*/
|
|
230
|
+
const csrfCookie = generateCsrfCookie(userIdResult.csrfToken, {
|
|
231
|
+
hostOrigin: (await this.config.generateServiceOrigin?.({
|
|
232
|
+
requestHeaders,
|
|
233
|
+
})) || this.config.serviceOrigin,
|
|
234
|
+
isDev: this.config.isDev,
|
|
235
|
+
});
|
|
225
236
|
return {
|
|
226
237
|
user: assumedUser || user,
|
|
227
238
|
isAssumed: !!assumedUser,
|
|
228
|
-
responseHeaders:
|
|
239
|
+
responseHeaders: {
|
|
240
|
+
'set-cookie': mergeHeaderValues(cookieRefreshHeaders?.['set-cookie'], csrfCookie),
|
|
241
|
+
},
|
|
229
242
|
};
|
|
230
243
|
}
|
|
231
244
|
/**
|
|
@@ -273,18 +286,22 @@ export class BackendAuthClient {
|
|
|
273
286
|
* generating a new one.
|
|
274
287
|
*/
|
|
275
288
|
async refreshLoginHeaders({ userId, cookieParams, existingUserIdResult, }) {
|
|
276
|
-
const
|
|
289
|
+
const authCookie = await generateAuthCookie({
|
|
277
290
|
csrfToken: existingUserIdResult.csrfToken,
|
|
278
291
|
userId,
|
|
279
292
|
sessionStartedAt: existingUserIdResult.sessionStartedAt,
|
|
280
293
|
}, cookieParams);
|
|
294
|
+
const csrfCookie = generateCsrfCookie(existingUserIdResult.csrfToken, cookieParams);
|
|
281
295
|
return {
|
|
282
|
-
'set-cookie':
|
|
296
|
+
'set-cookie': [
|
|
297
|
+
authCookie,
|
|
298
|
+
csrfCookie,
|
|
299
|
+
],
|
|
283
300
|
};
|
|
284
301
|
}
|
|
285
302
|
/** Use these headers to log a user in. */
|
|
286
303
|
async createLoginHeaders({ userId, requestHeaders, isSignUpCookie, }) {
|
|
287
|
-
const oppositeCookieName = isSignUpCookie ?
|
|
304
|
+
const oppositeCookieName = isSignUpCookie ? AuthCookie.Auth : AuthCookie.SignUp;
|
|
288
305
|
const hasExistingOppositeCookie = requestHeaders.cookie?.includes(`${oppositeCookieName}=`);
|
|
289
306
|
const discardOppositeCookieHeaders = hasExistingOppositeCookie
|
|
290
307
|
? generateLogoutHeaders(await this.getCookieParams({
|
|
@@ -292,7 +309,7 @@ export class BackendAuthClient {
|
|
|
292
309
|
requestHeaders,
|
|
293
310
|
}))
|
|
294
311
|
: undefined;
|
|
295
|
-
const existingUserIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), this.config.csrf, isSignUpCookie ?
|
|
312
|
+
const existingUserIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), this.config.csrf, isSignUpCookie ? AuthCookie.SignUp : AuthCookie.Auth);
|
|
296
313
|
const cookieParams = await this.getCookieParams({
|
|
297
314
|
isSignUpCookie,
|
|
298
315
|
requestHeaders,
|
|
@@ -303,7 +320,7 @@ export class BackendAuthClient {
|
|
|
303
320
|
cookieParams,
|
|
304
321
|
existingUserIdResult,
|
|
305
322
|
})
|
|
306
|
-
: await generateSuccessfulLoginHeaders(userId, cookieParams
|
|
323
|
+
: await generateSuccessfulLoginHeaders(userId, cookieParams);
|
|
307
324
|
return {
|
|
308
325
|
...newCookieHeaders,
|
|
309
326
|
'set-cookie': mergeHeaderValues(newCookieHeaders['set-cookie'], discardOppositeCookieHeaders?.['set-cookie']),
|
|
@@ -337,7 +354,7 @@ export class BackendAuthClient {
|
|
|
337
354
|
*/
|
|
338
355
|
async getInsecureUser({ requestHeaders, allowUserAuthRefresh, }) {
|
|
339
356
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
340
|
-
const userIdResult = await insecureExtractUserIdFromCookieAlone(requestHeaders, await this.getJwtParams(),
|
|
357
|
+
const userIdResult = await insecureExtractUserIdFromCookieAlone(requestHeaders, await this.getJwtParams(), AuthCookie.Auth);
|
|
341
358
|
if (!userIdResult) {
|
|
342
359
|
this.logForUser({
|
|
343
360
|
user: undefined,
|
|
@@ -1,7 +1,6 @@
|
|
|
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 CsrfTokenStore } from '../csrf-token-store.js';
|
|
5
4
|
import { type CsrfHeaderNameOption } from '../csrf-token.js';
|
|
6
5
|
/**
|
|
7
6
|
* Config for {@link FrontendAuthClient}.
|
|
@@ -49,16 +48,12 @@ export type FrontendAuthClientConfig = Readonly<{
|
|
|
49
48
|
* another user.
|
|
50
49
|
*/
|
|
51
50
|
assumedUserHeaderName: string;
|
|
52
|
-
/**
|
|
53
|
-
* Allowed clock skew tolerance for CSRF token expiration checks. Accounts for differences
|
|
54
|
-
* between server and client clocks.
|
|
55
|
-
*
|
|
56
|
-
* @default {minutes: 5}
|
|
57
|
-
*/
|
|
58
|
-
allowedClockSkew: Readonly<AnyDuration>;
|
|
59
51
|
overrides: PartialWithUndefined<{
|
|
60
|
-
localStorage:
|
|
61
|
-
|
|
52
|
+
localStorage: SelectFrom<Storage, {
|
|
53
|
+
setItem: true;
|
|
54
|
+
removeItem: true;
|
|
55
|
+
getItem: true;
|
|
56
|
+
}>;
|
|
62
57
|
}>;
|
|
63
58
|
}>;
|
|
64
59
|
/**
|
|
@@ -79,8 +74,6 @@ export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatible
|
|
|
79
74
|
* interval).
|
|
80
75
|
*/
|
|
81
76
|
destroy(): void;
|
|
82
|
-
/** Wraps {@link getCurrentCsrfToken} to automatically handle wiping an invalid CSRF token. */
|
|
83
|
-
getCurrentCsrfToken(): Promise<string | undefined>;
|
|
84
77
|
/**
|
|
85
78
|
* Assume the given user. Pass `undefined` to wipe the currently assumed user.
|
|
86
79
|
*
|
|
@@ -95,17 +88,16 @@ export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatible
|
|
|
95
88
|
* `@augment-vir/common`](https://electrovir.github.io/augment-vir/functions/mergeDeep.html) to
|
|
96
89
|
* combine them with these.
|
|
97
90
|
*/
|
|
98
|
-
createAuthenticatedRequestInit():
|
|
91
|
+
createAuthenticatedRequestInit(): RequestInit;
|
|
99
92
|
/** Wipes the current user auth. */
|
|
100
93
|
logout(): Promise<void>;
|
|
101
94
|
/**
|
|
102
|
-
* Use to handle a login response.
|
|
95
|
+
* Use to handle a login response. The CSRF token cookie is automatically stored by the browser
|
|
96
|
+
* from the `Set-Cookie` response header.
|
|
103
97
|
*
|
|
104
98
|
* @throws Error if the login response failed.
|
|
105
|
-
* @throws Error if the login response has an invalid CSRF token.
|
|
106
99
|
*/
|
|
107
100
|
handleLoginResponse(response: Readonly<SelectFrom<Response, {
|
|
108
|
-
headers: true;
|
|
109
101
|
ok: true;
|
|
110
102
|
}>>): Promise<void>;
|
|
111
103
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HttpStatus, } from '@augment-vir/common';
|
|
2
2
|
import { listenToActivity } from 'detect-activity';
|
|
3
|
-
import {
|
|
3
|
+
import { getCurrentCsrfToken, resolveCsrfHeaderName, } from '../csrf-token.js';
|
|
4
4
|
import { AuthHeaderName } from '../headers.js';
|
|
5
5
|
/**
|
|
6
6
|
* An auth client for sending and validating client requests to a backend. This should only be used
|
|
@@ -41,18 +41,6 @@ export class FrontendAuthClient {
|
|
|
41
41
|
this.userCheckInterval?.clearInterval();
|
|
42
42
|
this.removeActivityListener?.();
|
|
43
43
|
}
|
|
44
|
-
/** Wraps {@link getCurrentCsrfToken} to automatically handle wiping an invalid CSRF token. */
|
|
45
|
-
async getCurrentCsrfToken() {
|
|
46
|
-
const csrfTokenResult = await getCurrentCsrfToken({
|
|
47
|
-
...this.config.csrf,
|
|
48
|
-
csrfTokenStore: this.config.overrides?.csrfTokenStore,
|
|
49
|
-
allowedClockSkew: this.config.allowedClockSkew || defaultAllowedClockSkew,
|
|
50
|
-
});
|
|
51
|
-
if (csrfTokenResult.failure) {
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
54
|
-
return csrfTokenResult.csrfToken.token;
|
|
55
|
-
}
|
|
56
44
|
/**
|
|
57
45
|
* Assume the given user. Pass `undefined` to wipe the currently assumed user.
|
|
58
46
|
*
|
|
@@ -90,8 +78,8 @@ export class FrontendAuthClient {
|
|
|
90
78
|
* `@augment-vir/common`](https://electrovir.github.io/augment-vir/functions/mergeDeep.html) to
|
|
91
79
|
* combine them with these.
|
|
92
80
|
*/
|
|
93
|
-
|
|
94
|
-
const csrfToken =
|
|
81
|
+
createAuthenticatedRequestInit() {
|
|
82
|
+
const csrfToken = getCurrentCsrfToken();
|
|
95
83
|
const assumedUser = this.getAssumedUser();
|
|
96
84
|
const headers = {
|
|
97
85
|
...(csrfToken
|
|
@@ -115,27 +103,16 @@ export class FrontendAuthClient {
|
|
|
115
103
|
await this.config.authClearedCallback?.();
|
|
116
104
|
}
|
|
117
105
|
/**
|
|
118
|
-
* Use to handle a login response.
|
|
106
|
+
* Use to handle a login response. The CSRF token cookie is automatically stored by the browser
|
|
107
|
+
* from the `Set-Cookie` response header.
|
|
119
108
|
*
|
|
120
109
|
* @throws Error if the login response failed.
|
|
121
|
-
* @throws Error if the login response has an invalid CSRF token.
|
|
122
110
|
*/
|
|
123
111
|
async handleLoginResponse(response) {
|
|
124
112
|
if (!response.ok) {
|
|
125
113
|
await this.logout();
|
|
126
114
|
throw new Error('Login response failed.');
|
|
127
115
|
}
|
|
128
|
-
const { csrfToken } = extractCsrfTokenHeader(response, this.config.csrf, {
|
|
129
|
-
allowedClockSkew: this.config.allowedClockSkew || defaultAllowedClockSkew,
|
|
130
|
-
});
|
|
131
|
-
if (!csrfToken) {
|
|
132
|
-
await this.logout();
|
|
133
|
-
throw new Error('Did not receive any CSRF token.');
|
|
134
|
-
}
|
|
135
|
-
await storeCsrfToken(csrfToken, {
|
|
136
|
-
...this.config.csrf,
|
|
137
|
-
csrfTokenStore: this.config.overrides?.csrfTokenStore,
|
|
138
|
-
});
|
|
139
116
|
}
|
|
140
117
|
/**
|
|
141
118
|
* Use to verify _all_ responses received from the backend. Immediately logs the user out once
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type SelectFrom } from '@augment-vir/common';
|
|
2
2
|
import { type FullDate, type UtcTimezone } from 'date-vir';
|
|
3
|
-
import { type CookieParams } from './cookie.js';
|
|
4
|
-
import { type CsrfTokenStore } from './csrf-token-store.js';
|
|
3
|
+
import { AuthCookie, type CookieParams } from './cookie.js';
|
|
5
4
|
import { type CsrfHeaderNameOption } from './csrf-token.js';
|
|
6
5
|
import { type ParseJwtParams } from './jwt/jwt.js';
|
|
7
6
|
import { type JwtUserData } from './jwt/user-jwt.js';
|
|
@@ -21,7 +20,7 @@ export type UserIdResult<UserId extends string | number> = {
|
|
|
21
20
|
jwtExpiration: FullDate<UtcTimezone>;
|
|
22
21
|
/** When the JWT was issued (`iat` claim). */
|
|
23
22
|
jwtIssuedAt: FullDate<UtcTimezone>;
|
|
24
|
-
cookieName:
|
|
23
|
+
cookieName: AuthCookie;
|
|
25
24
|
/** The CSRF token embedded in the JWT. */
|
|
26
25
|
csrfToken: string;
|
|
27
26
|
/**
|
|
@@ -38,7 +37,7 @@ export type UserIdResult<UserId extends string | number> = {
|
|
|
38
37
|
* @category Auth : Host
|
|
39
38
|
* @returns The extracted user id or `undefined` if no valid auth headers exist.
|
|
40
39
|
*/
|
|
41
|
-
export declare function extractUserIdFromRequestHeaders<UserId extends string | number>(headers: HeaderContainer, jwtParams: Readonly<ParseJwtParams>, csrfHeaderNameOption: Readonly<CsrfHeaderNameOption>, cookieName?:
|
|
40
|
+
export declare function extractUserIdFromRequestHeaders<UserId extends string | number>(headers: HeaderContainer, jwtParams: Readonly<ParseJwtParams>, csrfHeaderNameOption: Readonly<CsrfHeaderNameOption>, cookieName?: AuthCookie): Promise<Readonly<UserIdResult<UserId>> | undefined>;
|
|
42
41
|
/**
|
|
43
42
|
* Extract a user id from just the cookie, without CSRF token validation. This is _less secure_ than
|
|
44
43
|
* {@link extractUserIdFromRequestHeaders} as a result. This should only be used in rare
|
|
@@ -47,41 +46,29 @@ export declare function extractUserIdFromRequestHeaders<UserId extends string |
|
|
|
47
46
|
* @deprecated Prefer {@link extractUserIdFromRequestHeaders} instead: it is more secure.
|
|
48
47
|
* @category Auth : Host
|
|
49
48
|
*/
|
|
50
|
-
export declare function insecureExtractUserIdFromCookieAlone<UserId extends string | number>(headers: HeaderContainer, jwtParams: Readonly<ParseJwtParams>, cookieName
|
|
49
|
+
export declare function insecureExtractUserIdFromCookieAlone<UserId extends string | number>(headers: HeaderContainer, jwtParams: Readonly<ParseJwtParams>, cookieName: AuthCookie): Promise<Readonly<UserIdResult<UserId>> | undefined>;
|
|
51
50
|
/**
|
|
52
|
-
* Used by host (backend) code to set headers on a response object.
|
|
51
|
+
* Used by host (backend) code to set headers on a response object. Sets both the auth JWT cookie
|
|
52
|
+
* and the CSRF token cookie. The CSRF cookie is not `HttpOnly` so that frontend JavaScript can read
|
|
53
|
+
* it and inject the value as a request header.
|
|
53
54
|
*
|
|
54
55
|
* @category Auth : Host
|
|
55
56
|
*/
|
|
56
57
|
export declare function generateSuccessfulLoginHeaders(
|
|
57
58
|
/** The id from your database of the user you're authenticating. */
|
|
58
|
-
userId: string | number, cookieConfig: Readonly<CookieParams>,
|
|
59
|
+
userId: string | number, cookieConfig: Readonly<CookieParams>,
|
|
59
60
|
/**
|
|
60
61
|
* The timestamp (in seconds) when the session originally started. If not provided, the current
|
|
61
62
|
* time will be used (for new sessions).
|
|
62
63
|
*/
|
|
63
|
-
sessionStartedAt?: number | undefined): Promise<Record<string, string>>;
|
|
64
|
+
sessionStartedAt?: number | undefined): Promise<Record<string, string[]>>;
|
|
64
65
|
/**
|
|
65
66
|
* Used by host (backend) code to set headers on a response object when the user has logged out or
|
|
66
67
|
* failed to authorize.
|
|
67
68
|
*
|
|
68
69
|
* @category Auth : Host
|
|
69
70
|
*/
|
|
70
|
-
export declare function generateLogoutHeaders(cookieConfig: Readonly<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
* Alternatively, if the given response failed, this will wipe the existing (if any) stored CSRF
|
|
75
|
-
* token.
|
|
76
|
-
*
|
|
77
|
-
* @category Auth : Client
|
|
78
|
-
* @throws Error if no CSRF token header is found.
|
|
79
|
-
*/
|
|
80
|
-
export declare function handleAuthResponse(response: Readonly<Pick<Response, 'ok' | 'headers'>>, options: Readonly<CsrfHeaderNameOption> & PartialWithUndefined<{
|
|
81
|
-
/**
|
|
82
|
-
* Allows mocking or overriding the default CSRF token store.
|
|
83
|
-
*
|
|
84
|
-
* @default getDefaultCsrfTokenStore()
|
|
85
|
-
*/
|
|
86
|
-
csrfTokenStore: CsrfTokenStore;
|
|
87
|
-
}>): Promise<void>;
|
|
71
|
+
export declare function generateLogoutHeaders(cookieConfig: Readonly<SelectFrom<CookieParams, {
|
|
72
|
+
hostOrigin: true;
|
|
73
|
+
isDev: true;
|
|
74
|
+
}>>): Record<string, string[]>;
|
package/dist/auth.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { AuthCookie, clearAuthCookie, clearCsrfCookie, extractCookieJwt, generateAuthCookie, generateCsrfCookie, } from './cookie.js';
|
|
2
|
+
import { generateCsrfToken, resolveCsrfHeaderName } from './csrf-token.js';
|
|
3
3
|
function readHeader(headers, headerName) {
|
|
4
4
|
if (headers instanceof Headers) {
|
|
5
5
|
return headers.get(headerName) || undefined;
|
|
@@ -18,12 +18,7 @@ function readHeader(headers, headerName) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
function readCsrfTokenHeader(headers, csrfHeaderNameOption) {
|
|
21
|
-
|
|
22
|
-
if (!rawCsrfToken) {
|
|
23
|
-
return undefined;
|
|
24
|
-
}
|
|
25
|
-
const token = parseCsrfToken(rawCsrfToken).csrfToken?.token || rawCsrfToken;
|
|
26
|
-
return token;
|
|
21
|
+
return readHeader(headers, resolveCsrfHeaderName(csrfHeaderNameOption));
|
|
27
22
|
}
|
|
28
23
|
/**
|
|
29
24
|
* Extract the user id from a request by checking both the request cookie and CSRF token. This is
|
|
@@ -33,7 +28,7 @@ function readCsrfTokenHeader(headers, csrfHeaderNameOption) {
|
|
|
33
28
|
* @category Auth : Host
|
|
34
29
|
* @returns The extracted user id or `undefined` if no valid auth headers exist.
|
|
35
30
|
*/
|
|
36
|
-
export async function extractUserIdFromRequestHeaders(headers, jwtParams, csrfHeaderNameOption, cookieName =
|
|
31
|
+
export async function extractUserIdFromRequestHeaders(headers, jwtParams, csrfHeaderNameOption, cookieName = AuthCookie.Auth) {
|
|
37
32
|
try {
|
|
38
33
|
const csrfToken = readCsrfTokenHeader(headers, csrfHeaderNameOption);
|
|
39
34
|
const cookie = readHeader(headers, 'cookie');
|
|
@@ -65,7 +60,7 @@ export async function extractUserIdFromRequestHeaders(headers, jwtParams, csrfHe
|
|
|
65
60
|
* @deprecated Prefer {@link extractUserIdFromRequestHeaders} instead: it is more secure.
|
|
66
61
|
* @category Auth : Host
|
|
67
62
|
*/
|
|
68
|
-
export async function insecureExtractUserIdFromCookieAlone(headers, jwtParams, cookieName
|
|
63
|
+
export async function insecureExtractUserIdFromCookieAlone(headers, jwtParams, cookieName) {
|
|
69
64
|
try {
|
|
70
65
|
const cookie = readHeader(headers, 'cookie');
|
|
71
66
|
if (!cookie) {
|
|
@@ -89,31 +84,32 @@ export async function insecureExtractUserIdFromCookieAlone(headers, jwtParams, c
|
|
|
89
84
|
}
|
|
90
85
|
}
|
|
91
86
|
/**
|
|
92
|
-
* Used by host (backend) code to set headers on a response object.
|
|
87
|
+
* Used by host (backend) code to set headers on a response object. Sets both the auth JWT cookie
|
|
88
|
+
* and the CSRF token cookie. The CSRF cookie is not `HttpOnly` so that frontend JavaScript can read
|
|
89
|
+
* it and inject the value as a request header.
|
|
93
90
|
*
|
|
94
91
|
* @category Auth : Host
|
|
95
92
|
*/
|
|
96
93
|
export async function generateSuccessfulLoginHeaders(
|
|
97
94
|
/** The id from your database of the user you're authenticating. */
|
|
98
|
-
userId, cookieConfig,
|
|
95
|
+
userId, cookieConfig,
|
|
99
96
|
/**
|
|
100
97
|
* The timestamp (in seconds) when the session originally started. If not provided, the current
|
|
101
98
|
* time will be used (for new sessions).
|
|
102
99
|
*/
|
|
103
100
|
sessionStartedAt) {
|
|
104
|
-
const csrfToken = generateCsrfToken(
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
csrfToken: csrfToken.token,
|
|
101
|
+
const csrfToken = generateCsrfToken();
|
|
102
|
+
const authCookie = await generateAuthCookie({
|
|
103
|
+
csrfToken,
|
|
108
104
|
userId,
|
|
109
105
|
sessionStartedAt: sessionStartedAt ?? Date.now(),
|
|
110
106
|
}, cookieConfig);
|
|
107
|
+
const csrfCookie = generateCsrfCookie(csrfToken, cookieConfig);
|
|
111
108
|
return {
|
|
112
|
-
'set-cookie':
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}),
|
|
109
|
+
'set-cookie': [
|
|
110
|
+
authCookie,
|
|
111
|
+
csrfCookie,
|
|
112
|
+
],
|
|
117
113
|
};
|
|
118
114
|
}
|
|
119
115
|
/**
|
|
@@ -124,25 +120,9 @@ sessionStartedAt) {
|
|
|
124
120
|
*/
|
|
125
121
|
export function generateLogoutHeaders(cookieConfig) {
|
|
126
122
|
return {
|
|
127
|
-
'set-cookie':
|
|
123
|
+
'set-cookie': [
|
|
124
|
+
clearAuthCookie(cookieConfig),
|
|
125
|
+
clearCsrfCookie(cookieConfig),
|
|
126
|
+
],
|
|
128
127
|
};
|
|
129
128
|
}
|
|
130
|
-
/**
|
|
131
|
-
* Store auth data on a client (frontend) after receiving an auth response from the host (backend).
|
|
132
|
-
* Specifically, this stores the CSRF token into IndexedDB (which doesn't need to be a secret).
|
|
133
|
-
* Alternatively, if the given response failed, this will wipe the existing (if any) stored CSRF
|
|
134
|
-
* token.
|
|
135
|
-
*
|
|
136
|
-
* @category Auth : Client
|
|
137
|
-
* @throws Error if no CSRF token header is found.
|
|
138
|
-
*/
|
|
139
|
-
export async function handleAuthResponse(response, options) {
|
|
140
|
-
if (!response.ok) {
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
const { csrfToken } = extractCsrfTokenHeader(response, options);
|
|
144
|
-
if (!csrfToken) {
|
|
145
|
-
throw new Error('Did not receive any CSRF token.');
|
|
146
|
-
}
|
|
147
|
-
await storeCsrfToken(csrfToken, options);
|
|
148
|
-
}
|