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/dist/csrf-token.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { randomString, wrapInTry, } from '@augment-vir/common';
|
|
2
2
|
import { calculateRelativeDate, fullDateShape, getNowInUtcTimezone, isDateAfter, } from 'date-vir';
|
|
3
3
|
import { defineShape, parseJsonWithShape } from 'object-shape-tester';
|
|
4
|
-
import { AuthHeaderName } from './headers.js';
|
|
5
|
-
import { authLog } from './log.js';
|
|
6
4
|
/**
|
|
7
5
|
* Shape definition for {@link CsrfToken}.
|
|
8
6
|
*
|
|
@@ -12,6 +10,14 @@ export const csrfTokenShape = defineShape({
|
|
|
12
10
|
token: '',
|
|
13
11
|
expiration: fullDateShape,
|
|
14
12
|
});
|
|
13
|
+
/**
|
|
14
|
+
* Default allowed clock skew for CSRF token expiration checks. Accounts for differences between
|
|
15
|
+
* server and client clocks when checking token expiration.
|
|
16
|
+
*
|
|
17
|
+
* @category Internal
|
|
18
|
+
* @default {minutes: 5}
|
|
19
|
+
*/
|
|
20
|
+
export const defaultAllowedClockSkew = { minutes: 5 };
|
|
15
21
|
/**
|
|
16
22
|
* Generates a random, cryptographically secure CSRF token.
|
|
17
23
|
*
|
|
@@ -39,30 +45,48 @@ export var CsrfTokenFailureReason;
|
|
|
39
45
|
/** A CSRF token was found and parsed but is expired. */
|
|
40
46
|
CsrfTokenFailureReason["Expired"] = "expired";
|
|
41
47
|
})(CsrfTokenFailureReason || (CsrfTokenFailureReason = {}));
|
|
48
|
+
/**
|
|
49
|
+
* Resolves a {@link CsrfHeaderNameOption} to the actual header name string.
|
|
50
|
+
*
|
|
51
|
+
* @category Auth : Client
|
|
52
|
+
* @category Auth : Host
|
|
53
|
+
*/
|
|
54
|
+
export function resolveCsrfHeaderName(option) {
|
|
55
|
+
if ('csrfHeaderName' in option && option.csrfHeaderName) {
|
|
56
|
+
return option.csrfHeaderName;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
return [
|
|
60
|
+
option.csrfHeaderPrefix,
|
|
61
|
+
'auth-vir',
|
|
62
|
+
'csrf-token',
|
|
63
|
+
].join('-');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
42
66
|
/**
|
|
43
67
|
* Extract the CSRF token header from a response.
|
|
44
68
|
*
|
|
45
69
|
* @category Auth : Client
|
|
46
70
|
*/
|
|
47
|
-
export function extractCsrfTokenHeader(response,
|
|
48
|
-
const csrfTokenHeaderName =
|
|
71
|
+
export function extractCsrfTokenHeader(response, csrfHeaderNameOption, options) {
|
|
72
|
+
const csrfTokenHeaderName = resolveCsrfHeaderName(csrfHeaderNameOption);
|
|
49
73
|
const rawCsrfToken = response.headers?.get(csrfTokenHeaderName);
|
|
50
|
-
return parseCsrfToken(rawCsrfToken);
|
|
74
|
+
return parseCsrfToken(rawCsrfToken, options);
|
|
51
75
|
}
|
|
52
76
|
/**
|
|
53
77
|
* Stores the given CSRF token into local storage.
|
|
54
78
|
*
|
|
55
79
|
* @category Auth : Client
|
|
56
80
|
*/
|
|
57
|
-
export function storeCsrfToken(csrfToken,
|
|
58
|
-
(
|
|
81
|
+
export function storeCsrfToken(csrfToken, options) {
|
|
82
|
+
(options.localStorage || globalThis.localStorage).setItem(resolveCsrfHeaderName(options), JSON.stringify(csrfToken));
|
|
59
83
|
}
|
|
60
84
|
/**
|
|
61
85
|
* Parse a raw CSRF token JSON string.
|
|
62
86
|
*
|
|
63
87
|
* @category Internal
|
|
64
88
|
*/
|
|
65
|
-
export function parseCsrfToken(value) {
|
|
89
|
+
export function parseCsrfToken(value, options) {
|
|
66
90
|
if (!value) {
|
|
67
91
|
return {
|
|
68
92
|
failure: CsrfTokenFailureReason.DoesNotExist,
|
|
@@ -79,9 +103,10 @@ export function parseCsrfToken(value) {
|
|
|
79
103
|
failure: CsrfTokenFailureReason.ParseFailed,
|
|
80
104
|
};
|
|
81
105
|
}
|
|
106
|
+
const effectiveExpiration = calculateRelativeDate(csrfToken.expiration, options?.allowedClockSkew || defaultAllowedClockSkew);
|
|
82
107
|
if (isDateAfter({
|
|
83
108
|
fullDate: getNowInUtcTimezone(),
|
|
84
|
-
relativeTo:
|
|
109
|
+
relativeTo: effectiveExpiration,
|
|
85
110
|
})) {
|
|
86
111
|
return {
|
|
87
112
|
failure: CsrfTokenFailureReason.Expired,
|
|
@@ -97,9 +122,10 @@ export function parseCsrfToken(value) {
|
|
|
97
122
|
*
|
|
98
123
|
* @category Auth : Client
|
|
99
124
|
*/
|
|
100
|
-
export function getCurrentCsrfToken(
|
|
101
|
-
const rawCsrfToken = (
|
|
102
|
-
|
|
125
|
+
export function getCurrentCsrfToken(options) {
|
|
126
|
+
const rawCsrfToken = (options.localStorage || globalThis.localStorage).getItem(resolveCsrfHeaderName(options)) ||
|
|
127
|
+
undefined;
|
|
128
|
+
return parseCsrfToken(rawCsrfToken, options);
|
|
103
129
|
}
|
|
104
130
|
/**
|
|
105
131
|
* Wipes the current stored CSRF token. This should be used by client (frontend) code to logout a
|
|
@@ -107,7 +133,6 @@ export function getCurrentCsrfToken(overrides = {}) {
|
|
|
107
133
|
*
|
|
108
134
|
* @category Auth : Client
|
|
109
135
|
*/
|
|
110
|
-
export function wipeCurrentCsrfToken(
|
|
111
|
-
|
|
112
|
-
return (overrides.localStorage || globalThis.localStorage).removeItem(overrides.csrfHeaderName || AuthHeaderName.CsrfToken);
|
|
136
|
+
export function wipeCurrentCsrfToken(options) {
|
|
137
|
+
return (options.localStorage || globalThis.localStorage).removeItem(resolveCsrfHeaderName(options));
|
|
113
138
|
}
|
package/dist/hash.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertWrap } from '@augment-vir/assert';
|
|
1
2
|
import { mergeDefinedProperties, } from '@augment-vir/common';
|
|
2
3
|
import { argon2id, argon2Verify } from 'hash-wasm';
|
|
3
4
|
/**
|
|
@@ -7,8 +8,8 @@ import { argon2id, argon2Verify } from 'hash-wasm';
|
|
|
7
8
|
*/
|
|
8
9
|
export const defaultHashOptions = {
|
|
9
10
|
hashLength: 32,
|
|
10
|
-
iterations:
|
|
11
|
-
memorySize:
|
|
11
|
+
iterations: 2,
|
|
12
|
+
memorySize: 19_456,
|
|
12
13
|
parallelism: 1,
|
|
13
14
|
};
|
|
14
15
|
/**
|
|
@@ -22,11 +23,12 @@ export const defaultHashOptions = {
|
|
|
22
23
|
*/
|
|
23
24
|
export async function hashPassword(password, options = {}) {
|
|
24
25
|
const salt = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
25
|
-
|
|
26
|
+
const hash = await argon2id(mergeDefinedProperties(defaultHashOptions, options, {
|
|
26
27
|
outputType: 'encoded',
|
|
27
28
|
password: password.normalize(),
|
|
28
29
|
salt,
|
|
29
30
|
}));
|
|
31
|
+
return assertWrap.isTruthy(hash);
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
34
|
* A utility that provides more accurate string byte size than doing `string.length`.
|
|
@@ -45,6 +47,6 @@ export function getByteLength(input) {
|
|
|
45
47
|
export async function doesPasswordMatchHash({ password, hash, }) {
|
|
46
48
|
return await argon2Verify({
|
|
47
49
|
hash,
|
|
48
|
-
password,
|
|
50
|
+
password: password.normalize(),
|
|
49
51
|
});
|
|
50
52
|
}
|
package/dist/headers.d.ts
CHANGED
package/dist/headers.js
CHANGED
|
@@ -6,7 +6,6 @@ import { check } from '@augment-vir/assert';
|
|
|
6
6
|
*/
|
|
7
7
|
export var AuthHeaderName;
|
|
8
8
|
(function (AuthHeaderName) {
|
|
9
|
-
AuthHeaderName["CsrfToken"] = "csrf-token";
|
|
10
9
|
AuthHeaderName["AssumedUser"] = "assumed-user";
|
|
11
10
|
/**
|
|
12
11
|
* Used to track if the current user is signed in only with a sign-up cookie, which prevents us
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './auth-client/backend-auth.client.js';
|
|
2
2
|
export * from './auth-client/frontend-auth.client.js';
|
|
3
|
+
export * from './auth-client/is-session-refresh-ready.js';
|
|
3
4
|
export * from './auth.js';
|
|
4
5
|
export * from './cookie.js';
|
|
5
6
|
export * from './csrf-token.js';
|
|
@@ -8,5 +9,4 @@ export * from './headers.js';
|
|
|
8
9
|
export * from './jwt/jwt-keys.js';
|
|
9
10
|
export * from './jwt/jwt.js';
|
|
10
11
|
export * from './jwt/user-jwt.js';
|
|
11
|
-
export * from './log.js';
|
|
12
12
|
export * from './mock-local-storage.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './auth-client/backend-auth.client.js';
|
|
2
2
|
export * from './auth-client/frontend-auth.client.js';
|
|
3
|
+
export * from './auth-client/is-session-refresh-ready.js';
|
|
3
4
|
export * from './auth.js';
|
|
4
5
|
export * from './cookie.js';
|
|
5
6
|
export * from './csrf-token.js';
|
|
@@ -8,5 +9,4 @@ export * from './headers.js';
|
|
|
8
9
|
export * from './jwt/jwt-keys.js';
|
|
9
10
|
export * from './jwt/jwt.js';
|
|
10
11
|
export * from './jwt/user-jwt.js';
|
|
11
|
-
export * from './log.js';
|
|
12
12
|
export * from './mock-local-storage.js';
|
package/dist/jwt/jwt.d.ts
CHANGED
|
@@ -85,7 +85,15 @@ data: JwtData, params: Readonly<CreateJwtParams>): Promise<string>;
|
|
|
85
85
|
*
|
|
86
86
|
* @category Internal
|
|
87
87
|
*/
|
|
88
|
-
export type ParseJwtParams = Readonly<Pick<CreateJwtParams, 'issuer' | 'audience' | 'jwtKeys'
|
|
88
|
+
export type ParseJwtParams = Readonly<Pick<CreateJwtParams, 'issuer' | 'audience' | 'jwtKeys'>> & PartialWithUndefined<{
|
|
89
|
+
/**
|
|
90
|
+
* Allowed clock skew tolerance for JWT expiration and timestamp checks. Accounts for
|
|
91
|
+
* differences between server and client clocks.
|
|
92
|
+
*
|
|
93
|
+
* @default {minutes: 5}
|
|
94
|
+
*/
|
|
95
|
+
allowedClockSkew: Readonly<AnyDuration>;
|
|
96
|
+
}>;
|
|
89
97
|
/**
|
|
90
98
|
* A fully parsed JWT with embedded data.
|
|
91
99
|
*
|
|
@@ -94,6 +102,8 @@ export type ParseJwtParams = Readonly<Pick<CreateJwtParams, 'issuer' | 'audience
|
|
|
94
102
|
export type ParsedJwt<JwtData extends AnyObject> = {
|
|
95
103
|
data: JwtData;
|
|
96
104
|
jwtExpiration: FullDate<UtcTimezone>;
|
|
105
|
+
/** When the JWT was issued (`iat` claim). */
|
|
106
|
+
jwtIssuedAt: FullDate<UtcTimezone>;
|
|
97
107
|
};
|
|
98
108
|
/**
|
|
99
109
|
* Parse and extract all data from an encrypted and signed JWT.
|
package/dist/jwt/jwt.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { assertWrap, check } from '@augment-vir/assert';
|
|
2
|
-
import { calculateRelativeDate, createFullDateInUserTimezone, createUtcFullDate, getNowInUtcTimezone, toTimestamp, } from 'date-vir';
|
|
2
|
+
import { calculateRelativeDate, convertDuration, createFullDateInUserTimezone, createUtcFullDate, getNowInUtcTimezone, toTimestamp, } from 'date-vir';
|
|
3
3
|
import { EncryptJWT, jwtDecrypt, jwtVerify, SignJWT } from 'jose';
|
|
4
|
+
import { defaultAllowedClockSkew } from '../csrf-token.js';
|
|
4
5
|
const encryptionProtectedHeader = { alg: 'dir', enc: 'A256GCM' };
|
|
5
6
|
const signingProtectedHeader = { alg: 'HS512' };
|
|
6
7
|
/**
|
|
@@ -57,6 +58,7 @@ export async function parseJwt(encryptedJwt, params) {
|
|
|
57
58
|
else if (!check.isString(decryptedJwt.payload.jwt)) {
|
|
58
59
|
throw new TypeError('Decrypted jwt is not a string.');
|
|
59
60
|
}
|
|
61
|
+
const clockToleranceSeconds = convertDuration(params.allowedClockSkew || defaultAllowedClockSkew, { seconds: true }).seconds;
|
|
60
62
|
const verifiedJwt = await jwtVerify(decryptedJwt.payload.jwt, params.jwtKeys.signingKey, {
|
|
61
63
|
issuer: params.issuer,
|
|
62
64
|
audience: params.audience,
|
|
@@ -65,18 +67,23 @@ export async function parseJwt(encryptedJwt, params) {
|
|
|
65
67
|
'aud',
|
|
66
68
|
'iss',
|
|
67
69
|
],
|
|
70
|
+
clockTolerance: clockToleranceSeconds,
|
|
68
71
|
});
|
|
69
|
-
if (!verifiedJwt.payload.iat ||
|
|
72
|
+
if (!verifiedJwt.payload.iat ||
|
|
73
|
+
verifiedJwt.payload.iat * 1000 > Date.now() + clockToleranceSeconds * 1000) {
|
|
70
74
|
throw new Error('"iat" claim timestamp check failed');
|
|
71
75
|
}
|
|
72
76
|
const data = verifiedJwt.payload.data;
|
|
73
77
|
if (!check.deepEquals(verifiedJwt.protectedHeader, signingProtectedHeader)) {
|
|
74
78
|
throw new Error('Invalid signing protected header.');
|
|
75
79
|
}
|
|
80
|
+
const issuedAtSeconds = assertWrap.isDefined(verifiedJwt.payload.iat, 'JWT has no issued at.');
|
|
76
81
|
const expirationSeconds = assertWrap.isDefined(verifiedJwt.payload.exp, 'JWT has no expiration.');
|
|
82
|
+
const jwtIssuedAt = parseJwtTimestamp(issuedAtSeconds);
|
|
77
83
|
const jwtExpiration = parseJwtTimestamp(expirationSeconds);
|
|
78
84
|
return {
|
|
79
85
|
data: data,
|
|
80
86
|
jwtExpiration,
|
|
87
|
+
jwtIssuedAt,
|
|
81
88
|
};
|
|
82
89
|
}
|
package/dist/jwt/user-jwt.js
CHANGED
|
@@ -39,12 +39,13 @@ export async function createUserJwt(data, params) {
|
|
|
39
39
|
* @category Internal
|
|
40
40
|
*/
|
|
41
41
|
export async function parseUserJwt(encryptedJwt, params) {
|
|
42
|
-
const { data, jwtExpiration } = await parseJwt(encryptedJwt, params);
|
|
42
|
+
const { data, jwtExpiration, jwtIssuedAt } = await parseJwt(encryptedJwt, params);
|
|
43
43
|
if (!checkValidShape(data, userJwtDataShape)) {
|
|
44
44
|
throw new TypeError('Verified jwt has wrong data.');
|
|
45
45
|
}
|
|
46
46
|
return {
|
|
47
47
|
data,
|
|
48
48
|
jwtExpiration,
|
|
49
|
+
jwtIssuedAt,
|
|
49
50
|
};
|
|
50
51
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auth-vir",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Auth made easy and secure via JWT cookies, CSRF tokens, and password hashing helpers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
"compile": "virmator compile",
|
|
35
35
|
"docs": "virmator docs",
|
|
36
36
|
"init": "node -e \"require('fs').rmSync('src/generated', { recursive: true, force: true })\" && prisma generate --no-hints --schema test-files/schema.prisma",
|
|
37
|
-
"start": "virmator frontend",
|
|
38
37
|
"test": "runstorm --names web,node, \"npm run test:web\" \"npm run test:node\"",
|
|
39
38
|
"test:docs": "virmator docs check",
|
|
40
39
|
"test:node": "virmator test node 'src/**/*.test.node.ts'",
|
|
@@ -42,21 +41,21 @@
|
|
|
42
41
|
"test:web": "virmator test web"
|
|
43
42
|
},
|
|
44
43
|
"dependencies": {
|
|
45
|
-
"@augment-vir/assert": "^31.
|
|
46
|
-
"@augment-vir/common": "^31.
|
|
47
|
-
"date-vir": "^8.1.
|
|
48
|
-
"detect-activity": "^0.0
|
|
44
|
+
"@augment-vir/assert": "^31.65.0",
|
|
45
|
+
"@augment-vir/common": "^31.65.0",
|
|
46
|
+
"date-vir": "^8.1.1",
|
|
47
|
+
"detect-activity": "^1.0.0",
|
|
49
48
|
"hash-wasm": "^4.12.0",
|
|
50
49
|
"jose": "^6.1.3",
|
|
51
50
|
"object-shape-tester": "^6.11.0",
|
|
52
|
-
"type-fest": "^5.4.
|
|
51
|
+
"type-fest": "^5.4.4",
|
|
53
52
|
"url-vir": "^2.1.7"
|
|
54
53
|
},
|
|
55
54
|
"devDependencies": {
|
|
56
|
-
"@augment-vir/test": "^31.
|
|
55
|
+
"@augment-vir/test": "^31.65.0",
|
|
57
56
|
"@prisma/client": "^6.19.2",
|
|
58
|
-
"@types/node": "^
|
|
59
|
-
"@web/dev-server-esbuild": "^1.0.
|
|
57
|
+
"@types/node": "^25.3.0",
|
|
58
|
+
"@web/dev-server-esbuild": "^1.0.5",
|
|
60
59
|
"@web/test-runner": "^0.20.2",
|
|
61
60
|
"@web/test-runner-commands": "^0.9.0",
|
|
62
61
|
"@web/test-runner-playwright": "^0.11.1",
|
|
@@ -64,7 +63,8 @@
|
|
|
64
63
|
"istanbul-smart-text-reporter": "^1.1.5",
|
|
65
64
|
"markdown-code-example-inserter": "^3.0.3",
|
|
66
65
|
"prisma-vir": "^2.3.3",
|
|
67
|
-
"typedoc": "^0.28.
|
|
66
|
+
"typedoc": "^0.28.17",
|
|
67
|
+
"typescript": "^5.9.3"
|
|
68
68
|
},
|
|
69
69
|
"engines": {
|
|
70
70
|
"node": ">=22"
|