auth-vir 4.0.0 → 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 +37 -24
- package/dist/auth-client/backend-auth.client.d.ts +1 -1
- package/dist/auth-client/backend-auth.client.js +40 -20
- package/dist/auth-client/frontend-auth.client.d.ts +8 -9
- package/dist/auth-client/frontend-auth.client.js +5 -21
- package/dist/auth.d.ts +14 -27
- package/dist/auth.js +18 -30
- package/dist/cookie.d.ts +41 -14
- package/dist/cookie.js +73 -31
- package/dist/csrf-token.d.ts +4 -57
- package/dist/csrf-token.js +16 -48
- 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 -27
- package/src/auth-client/frontend-auth.client.ts +6 -38
- package/src/auth.ts +25 -57
- package/src/cookie.ts +99 -48
- package/src/csrf-token.ts +19 -90
- 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 csrfToken =
|
|
319
|
+
const csrfToken = getCurrentCsrfToken();
|
|
306
320
|
|
|
307
321
|
if (!csrfToken) {
|
|
308
322
|
throw new Error('Not authenticated.');
|
|
@@ -317,22 +331,21 @@ export async function sendAuthenticatedRequest(
|
|
|
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,20 +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 } = 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
|
-
|
|
148
|
+
'set-cookie': [
|
|
149
|
+
authCookie,
|
|
150
|
+
csrfCookie,
|
|
151
|
+
],
|
|
150
152
|
};
|
|
151
153
|
}
|
|
152
154
|
else {
|
|
@@ -184,7 +186,7 @@ export class BackendAuthClient {
|
|
|
184
186
|
}
|
|
185
187
|
/** Securely extract a user from their request headers. */
|
|
186
188
|
async getSecureUser({ requestHeaders, isSignUpCookie, allowUserAuthRefresh, }) {
|
|
187
|
-
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);
|
|
188
190
|
if (!userIdResult) {
|
|
189
191
|
this.logForUser({
|
|
190
192
|
user: undefined,
|
|
@@ -215,14 +217,28 @@ export class BackendAuthClient {
|
|
|
215
217
|
requestHeaders,
|
|
216
218
|
user,
|
|
217
219
|
});
|
|
218
|
-
const cookieRefreshHeaders =
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
+
});
|
|
222
236
|
return {
|
|
223
237
|
user: assumedUser || user,
|
|
224
238
|
isAssumed: !!assumedUser,
|
|
225
|
-
responseHeaders:
|
|
239
|
+
responseHeaders: {
|
|
240
|
+
'set-cookie': mergeHeaderValues(cookieRefreshHeaders?.['set-cookie'], csrfCookie),
|
|
241
|
+
},
|
|
226
242
|
};
|
|
227
243
|
}
|
|
228
244
|
/**
|
|
@@ -270,18 +286,22 @@ export class BackendAuthClient {
|
|
|
270
286
|
* generating a new one.
|
|
271
287
|
*/
|
|
272
288
|
async refreshLoginHeaders({ userId, cookieParams, existingUserIdResult, }) {
|
|
273
|
-
const
|
|
289
|
+
const authCookie = await generateAuthCookie({
|
|
274
290
|
csrfToken: existingUserIdResult.csrfToken,
|
|
275
291
|
userId,
|
|
276
292
|
sessionStartedAt: existingUserIdResult.sessionStartedAt,
|
|
277
293
|
}, cookieParams);
|
|
294
|
+
const csrfCookie = generateCsrfCookie(existingUserIdResult.csrfToken, cookieParams);
|
|
278
295
|
return {
|
|
279
|
-
'set-cookie':
|
|
296
|
+
'set-cookie': [
|
|
297
|
+
authCookie,
|
|
298
|
+
csrfCookie,
|
|
299
|
+
],
|
|
280
300
|
};
|
|
281
301
|
}
|
|
282
302
|
/** Use these headers to log a user in. */
|
|
283
303
|
async createLoginHeaders({ userId, requestHeaders, isSignUpCookie, }) {
|
|
284
|
-
const oppositeCookieName = isSignUpCookie ?
|
|
304
|
+
const oppositeCookieName = isSignUpCookie ? AuthCookie.Auth : AuthCookie.SignUp;
|
|
285
305
|
const hasExistingOppositeCookie = requestHeaders.cookie?.includes(`${oppositeCookieName}=`);
|
|
286
306
|
const discardOppositeCookieHeaders = hasExistingOppositeCookie
|
|
287
307
|
? generateLogoutHeaders(await this.getCookieParams({
|
|
@@ -289,7 +309,7 @@ export class BackendAuthClient {
|
|
|
289
309
|
requestHeaders,
|
|
290
310
|
}))
|
|
291
311
|
: undefined;
|
|
292
|
-
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);
|
|
293
313
|
const cookieParams = await this.getCookieParams({
|
|
294
314
|
isSignUpCookie,
|
|
295
315
|
requestHeaders,
|
|
@@ -300,7 +320,7 @@ export class BackendAuthClient {
|
|
|
300
320
|
cookieParams,
|
|
301
321
|
existingUserIdResult,
|
|
302
322
|
})
|
|
303
|
-
: await generateSuccessfulLoginHeaders(userId, cookieParams
|
|
323
|
+
: await generateSuccessfulLoginHeaders(userId, cookieParams);
|
|
304
324
|
return {
|
|
305
325
|
...newCookieHeaders,
|
|
306
326
|
'set-cookie': mergeHeaderValues(newCookieHeaders['set-cookie'], discardOppositeCookieHeaders?.['set-cookie']),
|
|
@@ -334,7 +354,7 @@ export class BackendAuthClient {
|
|
|
334
354
|
*/
|
|
335
355
|
async getInsecureUser({ requestHeaders, allowUserAuthRefresh, }) {
|
|
336
356
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
337
|
-
const userIdResult = await insecureExtractUserIdFromCookieAlone(requestHeaders, await this.getJwtParams(),
|
|
357
|
+
const userIdResult = await insecureExtractUserIdFromCookieAlone(requestHeaders, await this.getJwtParams(), AuthCookie.Auth);
|
|
338
358
|
if (!userIdResult) {
|
|
339
359
|
this.logForUser({
|
|
340
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}.
|
|
@@ -50,8 +49,11 @@ export type FrontendAuthClientConfig = Readonly<{
|
|
|
50
49
|
*/
|
|
51
50
|
assumedUserHeaderName: string;
|
|
52
51
|
overrides: PartialWithUndefined<{
|
|
53
|
-
localStorage:
|
|
54
|
-
|
|
52
|
+
localStorage: SelectFrom<Storage, {
|
|
53
|
+
setItem: true;
|
|
54
|
+
removeItem: true;
|
|
55
|
+
getItem: true;
|
|
56
|
+
}>;
|
|
55
57
|
}>;
|
|
56
58
|
}>;
|
|
57
59
|
/**
|
|
@@ -72,8 +74,6 @@ export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatible
|
|
|
72
74
|
* interval).
|
|
73
75
|
*/
|
|
74
76
|
destroy(): void;
|
|
75
|
-
/** Wraps {@link getCurrentCsrfToken} to retrieve the stored CSRF token string. */
|
|
76
|
-
getCurrentCsrfToken(): Promise<string | undefined>;
|
|
77
77
|
/**
|
|
78
78
|
* Assume the given user. Pass `undefined` to wipe the currently assumed user.
|
|
79
79
|
*
|
|
@@ -88,17 +88,16 @@ export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatible
|
|
|
88
88
|
* `@augment-vir/common`](https://electrovir.github.io/augment-vir/functions/mergeDeep.html) to
|
|
89
89
|
* combine them with these.
|
|
90
90
|
*/
|
|
91
|
-
createAuthenticatedRequestInit():
|
|
91
|
+
createAuthenticatedRequestInit(): RequestInit;
|
|
92
92
|
/** Wipes the current user auth. */
|
|
93
93
|
logout(): Promise<void>;
|
|
94
94
|
/**
|
|
95
|
-
* 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.
|
|
96
97
|
*
|
|
97
98
|
* @throws Error if the login response failed.
|
|
98
|
-
* @throws Error if the login response has an invalid CSRF token.
|
|
99
99
|
*/
|
|
100
100
|
handleLoginResponse(response: Readonly<SelectFrom<Response, {
|
|
101
|
-
headers: true;
|
|
102
101
|
ok: true;
|
|
103
102
|
}>>): Promise<void>;
|
|
104
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,13 +41,6 @@ export class FrontendAuthClient {
|
|
|
41
41
|
this.userCheckInterval?.clearInterval();
|
|
42
42
|
this.removeActivityListener?.();
|
|
43
43
|
}
|
|
44
|
-
/** Wraps {@link getCurrentCsrfToken} to retrieve the stored CSRF token string. */
|
|
45
|
-
async getCurrentCsrfToken() {
|
|
46
|
-
return await getCurrentCsrfToken({
|
|
47
|
-
...this.config.csrf,
|
|
48
|
-
csrfTokenStore: this.config.overrides?.csrfTokenStore,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
44
|
/**
|
|
52
45
|
* Assume the given user. Pass `undefined` to wipe the currently assumed user.
|
|
53
46
|
*
|
|
@@ -85,8 +78,8 @@ export class FrontendAuthClient {
|
|
|
85
78
|
* `@augment-vir/common`](https://electrovir.github.io/augment-vir/functions/mergeDeep.html) to
|
|
86
79
|
* combine them with these.
|
|
87
80
|
*/
|
|
88
|
-
|
|
89
|
-
const csrfToken =
|
|
81
|
+
createAuthenticatedRequestInit() {
|
|
82
|
+
const csrfToken = getCurrentCsrfToken();
|
|
90
83
|
const assumedUser = this.getAssumedUser();
|
|
91
84
|
const headers = {
|
|
92
85
|
...(csrfToken
|
|
@@ -110,25 +103,16 @@ export class FrontendAuthClient {
|
|
|
110
103
|
await this.config.authClearedCallback?.();
|
|
111
104
|
}
|
|
112
105
|
/**
|
|
113
|
-
* 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.
|
|
114
108
|
*
|
|
115
109
|
* @throws Error if the login response failed.
|
|
116
|
-
* @throws Error if the login response has an invalid CSRF token.
|
|
117
110
|
*/
|
|
118
111
|
async handleLoginResponse(response) {
|
|
119
112
|
if (!response.ok) {
|
|
120
113
|
await this.logout();
|
|
121
114
|
throw new Error('Login response failed.');
|
|
122
115
|
}
|
|
123
|
-
const csrfToken = extractCsrfTokenHeader(response, this.config.csrf);
|
|
124
|
-
if (!csrfToken) {
|
|
125
|
-
await this.logout();
|
|
126
|
-
throw new Error('Did not receive any CSRF token.');
|
|
127
|
-
}
|
|
128
|
-
await storeCsrfToken(csrfToken, {
|
|
129
|
-
...this.config.csrf,
|
|
130
|
-
csrfTokenStore: this.config.overrides?.csrfTokenStore,
|
|
131
|
-
});
|
|
132
116
|
}
|
|
133
117
|
/**
|
|
134
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;
|
|
@@ -28,7 +28,7 @@ function readCsrfTokenHeader(headers, csrfHeaderNameOption) {
|
|
|
28
28
|
* @category Auth : Host
|
|
29
29
|
* @returns The extracted user id or `undefined` if no valid auth headers exist.
|
|
30
30
|
*/
|
|
31
|
-
export async function extractUserIdFromRequestHeaders(headers, jwtParams, csrfHeaderNameOption, cookieName =
|
|
31
|
+
export async function extractUserIdFromRequestHeaders(headers, jwtParams, csrfHeaderNameOption, cookieName = AuthCookie.Auth) {
|
|
32
32
|
try {
|
|
33
33
|
const csrfToken = readCsrfTokenHeader(headers, csrfHeaderNameOption);
|
|
34
34
|
const cookie = readHeader(headers, 'cookie');
|
|
@@ -60,7 +60,7 @@ export async function extractUserIdFromRequestHeaders(headers, jwtParams, csrfHe
|
|
|
60
60
|
* @deprecated Prefer {@link extractUserIdFromRequestHeaders} instead: it is more secure.
|
|
61
61
|
* @category Auth : Host
|
|
62
62
|
*/
|
|
63
|
-
export async function insecureExtractUserIdFromCookieAlone(headers, jwtParams, cookieName
|
|
63
|
+
export async function insecureExtractUserIdFromCookieAlone(headers, jwtParams, cookieName) {
|
|
64
64
|
try {
|
|
65
65
|
const cookie = readHeader(headers, 'cookie');
|
|
66
66
|
if (!cookie) {
|
|
@@ -84,28 +84,32 @@ export async function insecureExtractUserIdFromCookieAlone(headers, jwtParams, c
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
87
|
-
* 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.
|
|
88
90
|
*
|
|
89
91
|
* @category Auth : Host
|
|
90
92
|
*/
|
|
91
93
|
export async function generateSuccessfulLoginHeaders(
|
|
92
94
|
/** The id from your database of the user you're authenticating. */
|
|
93
|
-
userId, cookieConfig,
|
|
95
|
+
userId, cookieConfig,
|
|
94
96
|
/**
|
|
95
97
|
* The timestamp (in seconds) when the session originally started. If not provided, the current
|
|
96
98
|
* time will be used (for new sessions).
|
|
97
99
|
*/
|
|
98
100
|
sessionStartedAt) {
|
|
99
101
|
const csrfToken = generateCsrfToken();
|
|
100
|
-
const
|
|
101
|
-
const { cookie } = await generateAuthCookie({
|
|
102
|
+
const authCookie = await generateAuthCookie({
|
|
102
103
|
csrfToken,
|
|
103
104
|
userId,
|
|
104
105
|
sessionStartedAt: sessionStartedAt ?? Date.now(),
|
|
105
106
|
}, cookieConfig);
|
|
107
|
+
const csrfCookie = generateCsrfCookie(csrfToken, cookieConfig);
|
|
106
108
|
return {
|
|
107
|
-
'set-cookie':
|
|
108
|
-
|
|
109
|
+
'set-cookie': [
|
|
110
|
+
authCookie,
|
|
111
|
+
csrfCookie,
|
|
112
|
+
],
|
|
109
113
|
};
|
|
110
114
|
}
|
|
111
115
|
/**
|
|
@@ -116,25 +120,9 @@ sessionStartedAt) {
|
|
|
116
120
|
*/
|
|
117
121
|
export function generateLogoutHeaders(cookieConfig) {
|
|
118
122
|
return {
|
|
119
|
-
'set-cookie':
|
|
123
|
+
'set-cookie': [
|
|
124
|
+
clearAuthCookie(cookieConfig),
|
|
125
|
+
clearCsrfCookie(cookieConfig),
|
|
126
|
+
],
|
|
120
127
|
};
|
|
121
128
|
}
|
|
122
|
-
/**
|
|
123
|
-
* Store auth data on a client (frontend) after receiving an auth response from the host (backend).
|
|
124
|
-
* Specifically, this stores the CSRF token into IndexedDB (which doesn't need to be a secret).
|
|
125
|
-
* Alternatively, if the given response failed, this will wipe the existing (if any) stored CSRF
|
|
126
|
-
* token.
|
|
127
|
-
*
|
|
128
|
-
* @category Auth : Client
|
|
129
|
-
* @throws Error if no CSRF token header is found.
|
|
130
|
-
*/
|
|
131
|
-
export async function handleAuthResponse(response, options) {
|
|
132
|
-
if (!response.ok) {
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
const csrfToken = extractCsrfTokenHeader(response, options);
|
|
136
|
-
if (!csrfToken) {
|
|
137
|
-
throw new Error('Did not receive any CSRF token.');
|
|
138
|
-
}
|
|
139
|
-
await storeCsrfToken(csrfToken, options);
|
|
140
|
-
}
|
package/dist/cookie.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type PartialWithUndefined } from '@augment-vir/common';
|
|
2
|
-
import { type AnyDuration
|
|
1
|
+
import { type PartialWithUndefined, type SelectFrom } from '@augment-vir/common';
|
|
2
|
+
import { type AnyDuration } from 'date-vir';
|
|
3
3
|
import { type Primitive } from 'type-fest';
|
|
4
4
|
import { type CreateJwtParams, type ParseJwtParams, type ParsedJwt } from './jwt/jwt.js';
|
|
5
5
|
import { type JwtUserData } from './jwt/user-jwt.js';
|
|
@@ -8,11 +8,13 @@ import { type JwtUserData } from './jwt/user-jwt.js';
|
|
|
8
8
|
*
|
|
9
9
|
* @category Internal
|
|
10
10
|
*/
|
|
11
|
-
export declare enum
|
|
11
|
+
export declare enum AuthCookie {
|
|
12
12
|
/** Used for a full user login auth. */
|
|
13
13
|
Auth = "auth",
|
|
14
14
|
/** Use for a temporary "just signed up" auth. */
|
|
15
|
-
SignUp = "sign-up"
|
|
15
|
+
SignUp = "sign-up",
|
|
16
|
+
/** Used for storing the CSRF token. Not `HttpOnly` so that frontend JS can read it. */
|
|
17
|
+
Csrf = "auth-vir-csrf"
|
|
16
18
|
}
|
|
17
19
|
/**
|
|
18
20
|
* Parameters for {@link generateAuthCookie}.
|
|
@@ -38,8 +40,13 @@ export type CookieParams = {
|
|
|
38
40
|
* client, etc.
|
|
39
41
|
*/
|
|
40
42
|
jwtParams: Readonly<CreateJwtParams>;
|
|
41
|
-
cookieName?: string;
|
|
42
43
|
} & PartialWithUndefined<{
|
|
44
|
+
/**
|
|
45
|
+
* Which auth cookie name to use.
|
|
46
|
+
*
|
|
47
|
+
* @default AuthCookie.Auth
|
|
48
|
+
*/
|
|
49
|
+
authCookie: AuthCookie;
|
|
43
50
|
/**
|
|
44
51
|
* Is set to `true` (which should only be done in development environments), the cookie will be
|
|
45
52
|
* allowed in insecure requests (non HTTPS requests).
|
|
@@ -49,26 +56,46 @@ export type CookieParams = {
|
|
|
49
56
|
isDev: boolean;
|
|
50
57
|
}>;
|
|
51
58
|
/**
|
|
52
|
-
*
|
|
59
|
+
* Generate a secure cookie that stores the user JWT data. Used in host (backend) code.
|
|
53
60
|
*
|
|
54
61
|
* @category Internal
|
|
55
62
|
*/
|
|
56
|
-
export
|
|
57
|
-
cookie: string;
|
|
58
|
-
expiration: FullDate<UtcTimezone>;
|
|
59
|
-
};
|
|
63
|
+
export declare function generateAuthCookie(userJwtData: Readonly<JwtUserData>, cookieConfig: Readonly<CookieParams>): Promise<string>;
|
|
60
64
|
/**
|
|
61
|
-
* Generate a
|
|
65
|
+
* Generate a CSRF token cookie. This cookie is intentionally not `HttpOnly` so that frontend
|
|
66
|
+
* JavaScript can read it and inject the value as a request header for double-submit verification.
|
|
67
|
+
*
|
|
68
|
+
* The CSRF cookie uses a fixed 400-day MAX-AGE rather than matching the auth cookie duration. 400
|
|
69
|
+
* days is the cross-browser safe maximum (Chrome caps cookie lifetimes at 400 days; other browsers
|
|
70
|
+
* accept it as-is). The CSRF token is only meaningful when paired with a valid JWT, so it doesn't
|
|
71
|
+
* need its own expiration management. It gets regenerated on every fresh login.
|
|
62
72
|
*
|
|
63
73
|
* @category Internal
|
|
64
74
|
*/
|
|
65
|
-
export declare function
|
|
75
|
+
export declare function generateCsrfCookie(csrfToken: string, cookieConfig: Readonly<SelectFrom<CookieParams, {
|
|
76
|
+
hostOrigin: true;
|
|
77
|
+
isDev: true;
|
|
78
|
+
}>>): string;
|
|
66
79
|
/**
|
|
67
80
|
* Generate a cookie value that will clear the previous auth cookie. Use this when signing out.
|
|
68
81
|
*
|
|
69
82
|
* @category Internal
|
|
70
83
|
*/
|
|
71
|
-
export declare function clearAuthCookie(cookieConfig: Readonly<
|
|
84
|
+
export declare function clearAuthCookie(cookieConfig: Readonly<SelectFrom<CookieParams, {
|
|
85
|
+
hostOrigin: true;
|
|
86
|
+
isDev: true;
|
|
87
|
+
}>> & PartialWithUndefined<{
|
|
88
|
+
authCookie: AuthCookie;
|
|
89
|
+
}>): string;
|
|
90
|
+
/**
|
|
91
|
+
* Generate a cookie value that will clear the CSRF token cookie. Use this when signing out.
|
|
92
|
+
*
|
|
93
|
+
* @category Internal
|
|
94
|
+
*/
|
|
95
|
+
export declare function clearCsrfCookie(cookieConfig: Readonly<SelectFrom<CookieParams, {
|
|
96
|
+
hostOrigin: true;
|
|
97
|
+
isDev: true;
|
|
98
|
+
}>>): string;
|
|
72
99
|
/**
|
|
73
100
|
* Generate a cookie string from a raw set of parameters.
|
|
74
101
|
*
|
|
@@ -81,4 +108,4 @@ export declare function generateCookie(params: Readonly<Record<string, Exclude<P
|
|
|
81
108
|
* @category Internal
|
|
82
109
|
* @returns The extracted auth Cookie JWT data or `undefined` if no valid auth JWT data was found.
|
|
83
110
|
*/
|
|
84
|
-
export declare function extractCookieJwt(rawCookie: string, jwtParams: Readonly<ParseJwtParams>, cookieName
|
|
111
|
+
export declare function extractCookieJwt(rawCookie: string, jwtParams: Readonly<ParseJwtParams>, cookieName: AuthCookie): Promise<undefined | ParsedJwt<JwtUserData>>;
|