auth-vir 2.3.4 → 2.3.6
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/dist/auth-client/backend-auth.client.d.ts +1 -1
- package/dist/auth-client/backend-auth.client.js +4 -3
- package/dist/auth-client/frontend-auth.client.d.ts +7 -9
- package/dist/auth-client/frontend-auth.client.js +9 -10
- package/dist/csrf-token.d.ts +2 -2
- package/dist/csrf-token.js +1 -1
- package/package.json +6 -5
- package/src/auth-client/backend-auth.client.ts +6 -4
- package/src/auth-client/frontend-auth.client.ts +23 -21
- package/src/csrf-token.ts +2 -2
|
@@ -99,7 +99,7 @@ export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId exten
|
|
|
99
99
|
* How long before a user's session times out when we should start trying to refresh their
|
|
100
100
|
* session.
|
|
101
101
|
*
|
|
102
|
-
* @default {minutes:
|
|
102
|
+
* @default {minutes: 10}
|
|
103
103
|
*/
|
|
104
104
|
sessionRefreshThreshold: Readonly<AnyDuration>;
|
|
105
105
|
overrides: PartialWithUndefined<{
|
|
@@ -7,6 +7,9 @@ import { parseJwtKeys } from '../jwt/jwt-keys.js';
|
|
|
7
7
|
const defaultSessionIdleTimeout = {
|
|
8
8
|
minutes: 20,
|
|
9
9
|
};
|
|
10
|
+
const defaultSessionRefreshThreshold = {
|
|
11
|
+
minutes: 10,
|
|
12
|
+
};
|
|
10
13
|
/**
|
|
11
14
|
* An auth client for creating and validating JWTs embedded in cookies. This should only be used in
|
|
12
15
|
* a backend environment as it accesses native Node packages.
|
|
@@ -72,9 +75,7 @@ export class BackendAuthClient {
|
|
|
72
75
|
* - Z = JWT expiration outside the refresh threshold: {@link isRefreshReady} = false.
|
|
73
76
|
*/
|
|
74
77
|
const isRefreshReady = isDateAfter({
|
|
75
|
-
fullDate: calculateRelativeDate(now, this.config.sessionRefreshThreshold ||
|
|
76
|
-
minutes: 5,
|
|
77
|
-
}),
|
|
78
|
+
fullDate: calculateRelativeDate(now, this.config.sessionRefreshThreshold || defaultSessionRefreshThreshold),
|
|
78
79
|
relativeTo: userIdResult.jwtExpiration,
|
|
79
80
|
});
|
|
80
81
|
if (isRefreshReady) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createBlockingInterval, type JsonCompatibleObject, type MaybePromise, type PartialWithUndefined, type SelectFrom
|
|
1
|
+
import { 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
4
|
/**
|
|
@@ -23,10 +23,13 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
|
|
|
23
23
|
* Get a response from the backend to see if the user is still authenticated. If the
|
|
24
24
|
* response returns a non-authorized status, the user is wiped. Any other status is
|
|
25
25
|
* ignored.
|
|
26
|
+
*
|
|
27
|
+
* If the user is not currently authorized, this should return `undefined` to prevent
|
|
28
|
+
* unnecessary network traffic.
|
|
26
29
|
*/
|
|
27
30
|
performCheck: () => MaybePromise<SelectFrom<Response, {
|
|
28
31
|
status: true;
|
|
29
|
-
}
|
|
32
|
+
}> | undefined>;
|
|
30
33
|
/** @default {minutes: 1} */
|
|
31
34
|
interval?: AnyDuration | undefined;
|
|
32
35
|
};
|
|
@@ -46,11 +49,6 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
|
|
|
46
49
|
export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject = EmptyObject> {
|
|
47
50
|
protected readonly config: FrontendAuthClientConfig;
|
|
48
51
|
protected userCheckInterval: undefined | ReturnType<typeof createBlockingInterval>;
|
|
49
|
-
/**
|
|
50
|
-
* Keeps track of whether the latest state of auth is logged in (`true`) or not (`false`). This
|
|
51
|
-
* is used for determining if the check user interval should keep running.
|
|
52
|
-
*/
|
|
53
|
-
protected isLatestAuthorized: boolean;
|
|
54
52
|
constructor(config?: FrontendAuthClientConfig);
|
|
55
53
|
/**
|
|
56
54
|
* Destroys the client and performs all necessary cleanup (like clearing the user check
|
|
@@ -92,8 +90,8 @@ export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatible
|
|
|
92
90
|
*
|
|
93
91
|
* @returns `true` if the auth is okay, `false` otherwise.
|
|
94
92
|
*/
|
|
95
|
-
verifyResponseAuth(response: Readonly<
|
|
93
|
+
verifyResponseAuth(response: Readonly<PartialWithUndefined<SelectFrom<Response, {
|
|
96
94
|
status: true;
|
|
97
95
|
headers: true;
|
|
98
|
-
}
|
|
96
|
+
}>>>): Promise<boolean>;
|
|
99
97
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createBlockingInterval, HttpStatus, } from '@augment-vir/common';
|
|
2
|
+
import { isPageActive } from 'page-active';
|
|
2
3
|
import { CsrfTokenFailureReason, extractCsrfTokenHeader, getCurrentCsrfToken, storeCsrfToken, wipeCurrentCsrfToken, } from '../csrf-token.js';
|
|
3
4
|
import { AuthHeaderName } from '../headers.js';
|
|
4
5
|
/**
|
|
@@ -11,22 +12,17 @@ import { AuthHeaderName } from '../headers.js';
|
|
|
11
12
|
export class FrontendAuthClient {
|
|
12
13
|
config;
|
|
13
14
|
userCheckInterval;
|
|
14
|
-
/**
|
|
15
|
-
* Keeps track of whether the latest state of auth is logged in (`true`) or not (`false`). This
|
|
16
|
-
* is used for determining if the check user interval should keep running.
|
|
17
|
-
*/
|
|
18
|
-
isLatestAuthorized = false;
|
|
19
15
|
constructor(config = {}) {
|
|
20
16
|
this.config = config;
|
|
21
17
|
if (config.checkUser) {
|
|
22
18
|
this.userCheckInterval = createBlockingInterval(async () => {
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
if (!isPageActive()) {
|
|
20
|
+
/** Do not refresh the user when the page is inactive. */
|
|
25
21
|
return;
|
|
26
22
|
}
|
|
27
23
|
const response = await config.checkUser?.performCheck();
|
|
28
24
|
if (response) {
|
|
29
|
-
|
|
25
|
+
await this.verifyResponseAuth({
|
|
30
26
|
status: response.status,
|
|
31
27
|
});
|
|
32
28
|
}
|
|
@@ -111,7 +107,6 @@ export class FrontendAuthClient {
|
|
|
111
107
|
}
|
|
112
108
|
/** Wipes the current user auth. */
|
|
113
109
|
async logout() {
|
|
114
|
-
this.isLatestAuthorized = false;
|
|
115
110
|
await this.config.authClearedCallback?.();
|
|
116
111
|
wipeCurrentCsrfToken(this.config.overrides);
|
|
117
112
|
}
|
|
@@ -132,7 +127,6 @@ export class FrontendAuthClient {
|
|
|
132
127
|
throw new Error('Did not receive any CSRF token.');
|
|
133
128
|
}
|
|
134
129
|
storeCsrfToken(csrfToken, this.config.overrides);
|
|
135
|
-
this.isLatestAuthorized = true;
|
|
136
130
|
}
|
|
137
131
|
/**
|
|
138
132
|
* Use to verify _all_ responses received from the backend. Immediately logs the user out once
|
|
@@ -146,6 +140,11 @@ export class FrontendAuthClient {
|
|
|
146
140
|
await this.logout();
|
|
147
141
|
return false;
|
|
148
142
|
}
|
|
143
|
+
/** If the response has a new CSRF token, store it. */
|
|
144
|
+
const { csrfToken } = extractCsrfTokenHeader(response, this.config.overrides);
|
|
145
|
+
if (csrfToken) {
|
|
146
|
+
storeCsrfToken(csrfToken, this.config.overrides);
|
|
147
|
+
}
|
|
149
148
|
return true;
|
|
150
149
|
}
|
|
151
150
|
}
|
package/dist/csrf-token.d.ts
CHANGED
|
@@ -62,9 +62,9 @@ export type GetCsrfTokenResult = RequireExactlyOne<{
|
|
|
62
62
|
*
|
|
63
63
|
* @category Auth : Client
|
|
64
64
|
*/
|
|
65
|
-
export declare function extractCsrfTokenHeader(response: Readonly<SelectFrom<Response, {
|
|
65
|
+
export declare function extractCsrfTokenHeader(response: Readonly<PartialWithUndefined<SelectFrom<Response, {
|
|
66
66
|
headers: true;
|
|
67
|
-
}
|
|
67
|
+
}>>>, overrides?: PartialWithUndefined<{
|
|
68
68
|
csrfHeaderName: string;
|
|
69
69
|
}>): Readonly<GetCsrfTokenResult>;
|
|
70
70
|
/**
|
package/dist/csrf-token.js
CHANGED
|
@@ -45,7 +45,7 @@ export var CsrfTokenFailureReason;
|
|
|
45
45
|
*/
|
|
46
46
|
export function extractCsrfTokenHeader(response, overrides = {}) {
|
|
47
47
|
const csrfTokenHeaderName = overrides.csrfHeaderName || AuthHeaderName.CsrfToken;
|
|
48
|
-
const rawCsrfToken = response.headers
|
|
48
|
+
const rawCsrfToken = response.headers?.get(csrfTokenHeaderName);
|
|
49
49
|
return parseCsrfToken(rawCsrfToken);
|
|
50
50
|
}
|
|
51
51
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auth-vir",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.6",
|
|
4
4
|
"description": "Auth made easy and secure via JWT cookies, CSRF tokens, and password hashing helpers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|
|
@@ -42,17 +42,18 @@
|
|
|
42
42
|
"test:web": "virmator test web"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@augment-vir/assert": "^31.
|
|
46
|
-
"@augment-vir/common": "^31.
|
|
45
|
+
"@augment-vir/assert": "^31.47.0",
|
|
46
|
+
"@augment-vir/common": "^31.47.0",
|
|
47
47
|
"date-vir": "^8.0.0",
|
|
48
48
|
"hash-wasm": "^4.12.0",
|
|
49
49
|
"jose": "^6.1.0",
|
|
50
50
|
"object-shape-tester": "^6.9.3",
|
|
51
|
+
"page-active": "^1.0.3",
|
|
51
52
|
"type-fest": "^5.1.0",
|
|
52
53
|
"url-vir": "^2.1.6"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
|
-
"@augment-vir/test": "^31.
|
|
56
|
+
"@augment-vir/test": "^31.47.0",
|
|
56
57
|
"@prisma/client": "^6.18.0",
|
|
57
58
|
"@types/node": "^24.9.1",
|
|
58
59
|
"@web/dev-server-esbuild": "^1.0.4",
|
|
@@ -62,7 +63,7 @@
|
|
|
62
63
|
"@web/test-runner-visual-regression": "^0.10.0",
|
|
63
64
|
"istanbul-smart-text-reporter": "^1.1.5",
|
|
64
65
|
"markdown-code-example-inserter": "^3.0.3",
|
|
65
|
-
"prisma-vir": "^2.
|
|
66
|
+
"prisma-vir": "^2.1.0",
|
|
66
67
|
"typedoc": "^0.28.14"
|
|
67
68
|
},
|
|
68
69
|
"engines": {
|
|
@@ -123,7 +123,7 @@ export type BackendAuthClientConfig<
|
|
|
123
123
|
* How long before a user's session times out when we should start trying to refresh their
|
|
124
124
|
* session.
|
|
125
125
|
*
|
|
126
|
-
* @default {minutes:
|
|
126
|
+
* @default {minutes: 10}
|
|
127
127
|
*/
|
|
128
128
|
sessionRefreshThreshold: Readonly<AnyDuration>;
|
|
129
129
|
overrides: PartialWithUndefined<{
|
|
@@ -137,6 +137,10 @@ const defaultSessionIdleTimeout: Readonly<AnyDuration> = {
|
|
|
137
137
|
minutes: 20,
|
|
138
138
|
};
|
|
139
139
|
|
|
140
|
+
const defaultSessionRefreshThreshold: Readonly<AnyDuration> = {
|
|
141
|
+
minutes: 10,
|
|
142
|
+
};
|
|
143
|
+
|
|
140
144
|
/**
|
|
141
145
|
* An auth client for creating and validating JWTs embedded in cookies. This should only be used in
|
|
142
146
|
* a backend environment as it accesses native Node packages.
|
|
@@ -245,9 +249,7 @@ export class BackendAuthClient<
|
|
|
245
249
|
const isRefreshReady = isDateAfter({
|
|
246
250
|
fullDate: calculateRelativeDate(
|
|
247
251
|
now,
|
|
248
|
-
this.config.sessionRefreshThreshold ||
|
|
249
|
-
minutes: 5,
|
|
250
|
-
},
|
|
252
|
+
this.config.sessionRefreshThreshold || defaultSessionRefreshThreshold,
|
|
251
253
|
),
|
|
252
254
|
relativeTo: userIdResult.jwtExpiration,
|
|
253
255
|
});
|
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
type MaybePromise,
|
|
6
6
|
type PartialWithUndefined,
|
|
7
7
|
type SelectFrom,
|
|
8
|
-
type SetOptionalWithUndefined,
|
|
9
8
|
} from '@augment-vir/common';
|
|
10
9
|
import {type AnyDuration} from 'date-vir';
|
|
10
|
+
import {isPageActive} from 'page-active';
|
|
11
11
|
import {type EmptyObject} from 'type-fest';
|
|
12
12
|
import {
|
|
13
13
|
CsrfTokenFailureReason,
|
|
@@ -41,14 +41,18 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
|
|
|
41
41
|
* Get a response from the backend to see if the user is still authenticated. If the
|
|
42
42
|
* response returns a non-authorized status, the user is wiped. Any other status is
|
|
43
43
|
* ignored.
|
|
44
|
+
*
|
|
45
|
+
* If the user is not currently authorized, this should return `undefined` to prevent
|
|
46
|
+
* unnecessary network traffic.
|
|
44
47
|
*/
|
|
45
48
|
performCheck: () => MaybePromise<
|
|
46
|
-
SelectFrom<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
| SelectFrom<
|
|
50
|
+
Response,
|
|
51
|
+
{
|
|
52
|
+
status: true;
|
|
53
|
+
}
|
|
54
|
+
>
|
|
55
|
+
| undefined
|
|
52
56
|
>;
|
|
53
57
|
/** @default {minutes: 1} */
|
|
54
58
|
interval?: AnyDuration | undefined;
|
|
@@ -70,24 +74,19 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
|
|
|
70
74
|
*/
|
|
71
75
|
export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject = EmptyObject> {
|
|
72
76
|
protected userCheckInterval: undefined | ReturnType<typeof createBlockingInterval>;
|
|
73
|
-
/**
|
|
74
|
-
* Keeps track of whether the latest state of auth is logged in (`true`) or not (`false`). This
|
|
75
|
-
* is used for determining if the check user interval should keep running.
|
|
76
|
-
*/
|
|
77
|
-
protected isLatestAuthorized = false;
|
|
78
77
|
|
|
79
78
|
constructor(protected readonly config: FrontendAuthClientConfig = {}) {
|
|
80
79
|
if (config.checkUser) {
|
|
81
80
|
this.userCheckInterval = createBlockingInterval(
|
|
82
81
|
async () => {
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
if (!isPageActive()) {
|
|
83
|
+
/** Do not refresh the user when the page is inactive. */
|
|
85
84
|
return;
|
|
86
85
|
}
|
|
87
|
-
|
|
88
86
|
const response = await config.checkUser?.performCheck();
|
|
87
|
+
|
|
89
88
|
if (response) {
|
|
90
|
-
|
|
89
|
+
await this.verifyResponseAuth({
|
|
91
90
|
status: response.status,
|
|
92
91
|
});
|
|
93
92
|
}
|
|
@@ -194,7 +193,6 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
|
|
|
194
193
|
|
|
195
194
|
/** Wipes the current user auth. */
|
|
196
195
|
public async logout() {
|
|
197
|
-
this.isLatestAuthorized = false;
|
|
198
196
|
await this.config.authClearedCallback?.();
|
|
199
197
|
wipeCurrentCsrfToken(this.config.overrides);
|
|
200
198
|
}
|
|
@@ -229,7 +227,6 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
|
|
|
229
227
|
}
|
|
230
228
|
|
|
231
229
|
storeCsrfToken(csrfToken, this.config.overrides);
|
|
232
|
-
this.isLatestAuthorized = true;
|
|
233
230
|
}
|
|
234
231
|
|
|
235
232
|
/**
|
|
@@ -240,15 +237,14 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
|
|
|
240
237
|
*/
|
|
241
238
|
public async verifyResponseAuth(
|
|
242
239
|
response: Readonly<
|
|
243
|
-
|
|
240
|
+
PartialWithUndefined<
|
|
244
241
|
SelectFrom<
|
|
245
242
|
Response,
|
|
246
243
|
{
|
|
247
244
|
status: true;
|
|
248
245
|
headers: true;
|
|
249
246
|
}
|
|
250
|
-
|
|
251
|
-
'headers'
|
|
247
|
+
>
|
|
252
248
|
>
|
|
253
249
|
>,
|
|
254
250
|
): Promise<boolean> {
|
|
@@ -260,6 +256,12 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
|
|
|
260
256
|
return false;
|
|
261
257
|
}
|
|
262
258
|
|
|
259
|
+
/** If the response has a new CSRF token, store it. */
|
|
260
|
+
const {csrfToken} = extractCsrfTokenHeader(response, this.config.overrides);
|
|
261
|
+
if (csrfToken) {
|
|
262
|
+
storeCsrfToken(csrfToken, this.config.overrides);
|
|
263
|
+
}
|
|
264
|
+
|
|
263
265
|
return true;
|
|
264
266
|
}
|
|
265
267
|
}
|
package/src/csrf-token.ts
CHANGED
|
@@ -77,14 +77,14 @@ export type GetCsrfTokenResult = RequireExactlyOne<{
|
|
|
77
77
|
* @category Auth : Client
|
|
78
78
|
*/
|
|
79
79
|
export function extractCsrfTokenHeader(
|
|
80
|
-
response: Readonly<SelectFrom<Response, {headers: true}
|
|
80
|
+
response: Readonly<PartialWithUndefined<SelectFrom<Response, {headers: true}>>>,
|
|
81
81
|
overrides: PartialWithUndefined<{
|
|
82
82
|
csrfHeaderName: string;
|
|
83
83
|
}> = {},
|
|
84
84
|
): Readonly<GetCsrfTokenResult> {
|
|
85
85
|
const csrfTokenHeaderName = overrides.csrfHeaderName || AuthHeaderName.CsrfToken;
|
|
86
86
|
|
|
87
|
-
const rawCsrfToken = response.headers
|
|
87
|
+
const rawCsrfToken = response.headers?.get(csrfTokenHeaderName);
|
|
88
88
|
|
|
89
89
|
return parseCsrfToken(rawCsrfToken);
|
|
90
90
|
}
|