auth-vir 5.0.2 → 5.0.4
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 +263 -0
- package/dist/auth-client/backend-auth.client.js +398 -0
- package/dist/auth-client/frontend-auth.client.d.ts +113 -0
- package/dist/auth-client/frontend-auth.client.js +131 -0
- 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 +81 -0
- package/dist/auth.js +132 -0
- package/dist/cookie.d.ts +111 -0
- package/dist/cookie.js +137 -0
- package/dist/csrf-token.d.ts +33 -0
- package/dist/csrf-token.js +42 -0
- package/dist/generated/browser.d.ts +9 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.d.ts +26 -0
- package/dist/generated/client.js +32 -0
- package/dist/generated/commonInputTypes.d.ts +122 -0
- package/dist/generated/commonInputTypes.js +1 -0
- package/dist/generated/enums.d.ts +1 -0
- package/dist/generated/enums.js +10 -0
- package/dist/generated/internal/class.d.ts +126 -0
- package/dist/generated/internal/class.js +85 -0
- package/dist/generated/internal/prismaNamespace.d.ts +545 -0
- package/dist/generated/internal/prismaNamespace.js +102 -0
- package/dist/generated/internal/prismaNamespaceBrowser.d.ts +75 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +70 -0
- package/dist/generated/models/User.d.ts +980 -0
- package/dist/generated/models/User.js +1 -0
- package/dist/generated/models.d.ts +2 -0
- package/dist/generated/models.js +1 -0
- package/dist/generated/shapes.gen.d.ts +8 -0
- package/dist/generated/shapes.gen.js +11 -0
- package/dist/hash.d.ts +42 -0
- package/dist/hash.js +52 -0
- package/dist/headers.d.ts +19 -0
- package/dist/headers.js +32 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/jwt/jwt-keys.d.ts +44 -0
- package/dist/jwt/jwt-keys.js +57 -0
- package/dist/jwt/jwt-keys.script.d.ts +1 -0
- package/dist/jwt/jwt-keys.script.js +3 -0
- package/dist/jwt/jwt.d.ts +126 -0
- package/dist/jwt/jwt.js +109 -0
- package/dist/jwt/user-jwt.d.ts +44 -0
- package/dist/jwt/user-jwt.js +53 -0
- package/package.json +4 -4
- package/src/auth-client/backend-auth.client.ts +3 -0
- package/src/auth.ts +5 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type UserId } from './models/User.js';
|
|
2
|
+
export declare const UserIdShape: import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<UserId>>;
|
|
3
|
+
export declare const UserShape: import("object-shape-tester").Shape<{
|
|
4
|
+
id: import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<UserId>>;
|
|
5
|
+
createdAt: import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<`${number}-${number}-${number}T${number}:${number}:${number}.${number}Z`>>;
|
|
6
|
+
updatedAt: import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<`${number}-${number}-${number}T${number}:${number}:${number}.${number}Z`>>;
|
|
7
|
+
name: string;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** AUTO-GENERATED FILE. DO NOT EDIT DIRECTLY. */
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import { defineShape, typedStringShape } from 'object-shape-tester';
|
|
4
|
+
import { utcIsoStringShape } from 'date-vir';
|
|
5
|
+
export const UserIdShape = typedStringShape();
|
|
6
|
+
export const UserShape = defineShape({
|
|
7
|
+
id: UserIdShape,
|
|
8
|
+
createdAt: utcIsoStringShape(),
|
|
9
|
+
updatedAt: utcIsoStringShape(),
|
|
10
|
+
name: '',
|
|
11
|
+
});
|
package/dist/hash.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type PartialWithUndefined } from '@augment-vir/common';
|
|
2
|
+
import { type IArgon2Options } from 'hash-wasm';
|
|
3
|
+
/**
|
|
4
|
+
* Default value for {@link HashPasswordOptions}.
|
|
5
|
+
*
|
|
6
|
+
* @category Internal
|
|
7
|
+
*/
|
|
8
|
+
export declare const defaultHashOptions: HashPasswordOptions;
|
|
9
|
+
/**
|
|
10
|
+
* Options for {@link hashPassword}.
|
|
11
|
+
*
|
|
12
|
+
* @category Internal
|
|
13
|
+
*/
|
|
14
|
+
export type HashPasswordOptions = PartialWithUndefined<Omit<IArgon2Options, 'outputType' | 'salt' | 'password' | 'secret'>>;
|
|
15
|
+
/**
|
|
16
|
+
* Hashes a password using the Argon2id algorithm so passwords don't need to be stored in plain
|
|
17
|
+
* text. The output of this function is safe to store in a database for future credential
|
|
18
|
+
* comparisons.
|
|
19
|
+
*
|
|
20
|
+
* @category Auth : Host
|
|
21
|
+
* @returns The hashed password.
|
|
22
|
+
* @see https://en.wikipedia.org/wiki/Argon2
|
|
23
|
+
*/
|
|
24
|
+
export declare function hashPassword(password: string, options?: HashPasswordOptions): Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* A utility that provides more accurate string byte size than doing `string.length`.
|
|
27
|
+
*
|
|
28
|
+
* @category Internal
|
|
29
|
+
*/
|
|
30
|
+
export declare function getByteLength(input: string): number;
|
|
31
|
+
/**
|
|
32
|
+
* Checks if the given password is a match by comparing it to the previously computed and stored
|
|
33
|
+
* hash.
|
|
34
|
+
*
|
|
35
|
+
* @category Auth : Host
|
|
36
|
+
*/
|
|
37
|
+
export declare function doesPasswordMatchHash({ password, hash, }: {
|
|
38
|
+
/** The password entered by the user in their login attempt. */
|
|
39
|
+
password: string;
|
|
40
|
+
/** The stored password hash for that user. */
|
|
41
|
+
hash: string;
|
|
42
|
+
}): Promise<boolean>;
|
package/dist/hash.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { assertWrap } from '@augment-vir/assert';
|
|
2
|
+
import { mergeDefinedProperties, } from '@augment-vir/common';
|
|
3
|
+
import { argon2id, argon2Verify } from 'hash-wasm';
|
|
4
|
+
/**
|
|
5
|
+
* Default value for {@link HashPasswordOptions}.
|
|
6
|
+
*
|
|
7
|
+
* @category Internal
|
|
8
|
+
*/
|
|
9
|
+
export const defaultHashOptions = {
|
|
10
|
+
hashLength: 32,
|
|
11
|
+
iterations: 2,
|
|
12
|
+
memorySize: 19_456,
|
|
13
|
+
parallelism: 1,
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Hashes a password using the Argon2id algorithm so passwords don't need to be stored in plain
|
|
17
|
+
* text. The output of this function is safe to store in a database for future credential
|
|
18
|
+
* comparisons.
|
|
19
|
+
*
|
|
20
|
+
* @category Auth : Host
|
|
21
|
+
* @returns The hashed password.
|
|
22
|
+
* @see https://en.wikipedia.org/wiki/Argon2
|
|
23
|
+
*/
|
|
24
|
+
export async function hashPassword(password, options = {}) {
|
|
25
|
+
const salt = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
26
|
+
const hash = await argon2id(mergeDefinedProperties(defaultHashOptions, options, {
|
|
27
|
+
outputType: 'encoded',
|
|
28
|
+
password: password.normalize(),
|
|
29
|
+
salt,
|
|
30
|
+
}));
|
|
31
|
+
return assertWrap.isTruthy(hash);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A utility that provides more accurate string byte size than doing `string.length`.
|
|
35
|
+
*
|
|
36
|
+
* @category Internal
|
|
37
|
+
*/
|
|
38
|
+
export function getByteLength(input) {
|
|
39
|
+
return new Blob([input]).size;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Checks if the given password is a match by comparing it to the previously computed and stored
|
|
43
|
+
* hash.
|
|
44
|
+
*
|
|
45
|
+
* @category Auth : Host
|
|
46
|
+
*/
|
|
47
|
+
export async function doesPasswordMatchHash({ password, hash, }) {
|
|
48
|
+
return await argon2Verify({
|
|
49
|
+
hash,
|
|
50
|
+
password: password.normalize(),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* All custom headers used by auth-vir.
|
|
3
|
+
*
|
|
4
|
+
* @category Internal
|
|
5
|
+
*/
|
|
6
|
+
export declare enum AuthHeaderName {
|
|
7
|
+
AssumedUser = "assumed-user",
|
|
8
|
+
/**
|
|
9
|
+
* Used to track if the current user is signed in only with a sign-up cookie, which prevents us
|
|
10
|
+
* from prematurely wiping their CSRF token.
|
|
11
|
+
*/
|
|
12
|
+
IsSignUpAuth = "is-sign-up-auth"
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Merges multiple header values into a single array of header values.
|
|
16
|
+
*
|
|
17
|
+
* @category Internal
|
|
18
|
+
*/
|
|
19
|
+
export declare function mergeHeaderValues(...values: (string | string[] | undefined)[]): string[];
|
package/dist/headers.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { check } from '@augment-vir/assert';
|
|
2
|
+
/**
|
|
3
|
+
* All custom headers used by auth-vir.
|
|
4
|
+
*
|
|
5
|
+
* @category Internal
|
|
6
|
+
*/
|
|
7
|
+
export var AuthHeaderName;
|
|
8
|
+
(function (AuthHeaderName) {
|
|
9
|
+
AuthHeaderName["AssumedUser"] = "assumed-user";
|
|
10
|
+
/**
|
|
11
|
+
* Used to track if the current user is signed in only with a sign-up cookie, which prevents us
|
|
12
|
+
* from prematurely wiping their CSRF token.
|
|
13
|
+
*/
|
|
14
|
+
AuthHeaderName["IsSignUpAuth"] = "is-sign-up-auth";
|
|
15
|
+
})(AuthHeaderName || (AuthHeaderName = {}));
|
|
16
|
+
/**
|
|
17
|
+
* Merges multiple header values into a single array of header values.
|
|
18
|
+
*
|
|
19
|
+
* @category Internal
|
|
20
|
+
*/
|
|
21
|
+
export function mergeHeaderValues(...values) {
|
|
22
|
+
const finalHeaderValues = [];
|
|
23
|
+
values.forEach((value) => {
|
|
24
|
+
if (check.isArray(value)) {
|
|
25
|
+
finalHeaderValues.push(...value);
|
|
26
|
+
}
|
|
27
|
+
else if (check.isString(value)) {
|
|
28
|
+
finalHeaderValues.push(value);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return finalHeaderValues;
|
|
32
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './auth-client/backend-auth.client.js';
|
|
2
|
+
export * from './auth-client/frontend-auth.client.js';
|
|
3
|
+
export * from './auth-client/is-session-refresh-ready.js';
|
|
4
|
+
export * from './auth.js';
|
|
5
|
+
export * from './cookie.js';
|
|
6
|
+
export * from './csrf-token.js';
|
|
7
|
+
export * from './hash.js';
|
|
8
|
+
export * from './headers.js';
|
|
9
|
+
export * from './jwt/jwt-keys.js';
|
|
10
|
+
export * from './jwt/jwt.js';
|
|
11
|
+
export * from './jwt/user-jwt.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './auth-client/backend-auth.client.js';
|
|
2
|
+
export * from './auth-client/frontend-auth.client.js';
|
|
3
|
+
export * from './auth-client/is-session-refresh-ready.js';
|
|
4
|
+
export * from './auth.js';
|
|
5
|
+
export * from './cookie.js';
|
|
6
|
+
export * from './csrf-token.js';
|
|
7
|
+
export * from './hash.js';
|
|
8
|
+
export * from './headers.js';
|
|
9
|
+
export * from './jwt/jwt-keys.js';
|
|
10
|
+
export * from './jwt/jwt.js';
|
|
11
|
+
export * from './jwt/user-jwt.js';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The keys required to sign and encrypt the JWT in their raw form for storage in a secure secrets
|
|
3
|
+
* database (such as AWS Secrets Manager) for later parsing by {@link parseJwtKeys}.
|
|
4
|
+
*
|
|
5
|
+
* These keys should be kept secret and never shared with any frontend, client, etc.
|
|
6
|
+
*
|
|
7
|
+
* @category Internal
|
|
8
|
+
*/
|
|
9
|
+
export type RawJwtKeys = Readonly<{
|
|
10
|
+
encryptionKey: string;
|
|
11
|
+
signingKey: string;
|
|
12
|
+
}>;
|
|
13
|
+
/**
|
|
14
|
+
* The keys required to sign and encrypt the JWT.
|
|
15
|
+
*
|
|
16
|
+
* These keys should be kept secret and never shared with any frontend, client, etc.
|
|
17
|
+
*
|
|
18
|
+
* @category Internal
|
|
19
|
+
*/
|
|
20
|
+
export type JwtKeys = Readonly<{
|
|
21
|
+
/**
|
|
22
|
+
* Encryption key for JWTs. This is a Uint8Array because `EncryptJWT.encrypt` does not support
|
|
23
|
+
* `CryptoKey` for our chosen encryption algorithm.
|
|
24
|
+
*/
|
|
25
|
+
encryptionKey: Readonly<Uint8Array>;
|
|
26
|
+
/** Signing key for JWTs. */
|
|
27
|
+
signingKey: Readonly<CryptoKey>;
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* Generate fresh and serialized JWT signing and encryption keys. These should be stored in a secure
|
|
31
|
+
* secrets database (such as AWS Secrets Manager) for later parsing by {@link parseJwtKeys}.
|
|
32
|
+
*
|
|
33
|
+
* These keys should be kept secret and never shared with any frontend, client, etc.
|
|
34
|
+
*
|
|
35
|
+
* @category Keys
|
|
36
|
+
*/
|
|
37
|
+
export declare function generateNewJwtKeys(): Promise<RawJwtKeys>;
|
|
38
|
+
/**
|
|
39
|
+
* Parses an instance of {@link RawJwtKeys} and produces the final {@link JwtKeys} object required by
|
|
40
|
+
* all authentication functionality.
|
|
41
|
+
*
|
|
42
|
+
* @category Keys
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseJwtKeys(rawKeys: Readonly<RawJwtKeys>): Promise<Readonly<JwtKeys>>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { assertWrap } from '@augment-vir/assert';
|
|
2
|
+
import { base64url } from 'jose';
|
|
3
|
+
const signingKeyOptions = [
|
|
4
|
+
{
|
|
5
|
+
name: 'HMAC',
|
|
6
|
+
hash: 'SHA-512',
|
|
7
|
+
},
|
|
8
|
+
true,
|
|
9
|
+
[
|
|
10
|
+
'sign',
|
|
11
|
+
'verify',
|
|
12
|
+
],
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Generate fresh and serialized JWT signing and encryption keys. These should be stored in a secure
|
|
16
|
+
* secrets database (such as AWS Secrets Manager) for later parsing by {@link parseJwtKeys}.
|
|
17
|
+
*
|
|
18
|
+
* These keys should be kept secret and never shared with any frontend, client, etc.
|
|
19
|
+
*
|
|
20
|
+
* @category Keys
|
|
21
|
+
*/
|
|
22
|
+
export async function generateNewJwtKeys() {
|
|
23
|
+
return {
|
|
24
|
+
encryptionKey: assertWrap.isDefined((await globalThis.crypto.subtle.exportKey('jwk', await globalThis.crypto.subtle.generateKey({
|
|
25
|
+
name: 'AES-GCM',
|
|
26
|
+
length: 256,
|
|
27
|
+
}, true, [
|
|
28
|
+
'encrypt',
|
|
29
|
+
'decrypt',
|
|
30
|
+
]))).k),
|
|
31
|
+
signingKey: assertWrap.isDefined((await globalThis.crypto.subtle.exportKey('jwk', await globalThis.crypto.subtle.generateKey(...signingKeyOptions))).k),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parses an instance of {@link RawJwtKeys} and produces the final {@link JwtKeys} object required by
|
|
36
|
+
* all authentication functionality.
|
|
37
|
+
*
|
|
38
|
+
* @category Keys
|
|
39
|
+
*/
|
|
40
|
+
export async function parseJwtKeys(rawKeys) {
|
|
41
|
+
if (!rawKeys.encryptionKey) {
|
|
42
|
+
throw new Error('JWT encryption key is empty');
|
|
43
|
+
}
|
|
44
|
+
else if (!rawKeys.signingKey) {
|
|
45
|
+
throw new Error('JWT signing key is empty');
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
encryptionKey: base64url.decode(rawKeys.encryptionKey),
|
|
49
|
+
signingKey: await crypto.subtle.importKey('jwk', {
|
|
50
|
+
k: rawKeys.signingKey,
|
|
51
|
+
alg: 'HS512',
|
|
52
|
+
ext: signingKeyOptions[1],
|
|
53
|
+
key_ops: [...signingKeyOptions[2]],
|
|
54
|
+
kty: 'oct',
|
|
55
|
+
}, ...signingKeyOptions),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { type AnyObject, type PartialWithUndefined, type SelectFrom } from '@augment-vir/common';
|
|
2
|
+
import { type AnyDuration, type DateLike, type FullDate, type UtcTimezone } from 'date-vir';
|
|
3
|
+
import { type JwtKeys } from './jwt-keys.js';
|
|
4
|
+
/**
|
|
5
|
+
* Default allowed clock skew for JWT expiration checks. Accounts for differences between server and
|
|
6
|
+
* client clocks.
|
|
7
|
+
*
|
|
8
|
+
* @category Internal
|
|
9
|
+
* @default {minutes: 5}
|
|
10
|
+
*/
|
|
11
|
+
export declare const defaultAllowedClockSkew: Readonly<AnyDuration>;
|
|
12
|
+
/**
|
|
13
|
+
* Params for {@link createJwt}.
|
|
14
|
+
*
|
|
15
|
+
* @category Internal
|
|
16
|
+
*/
|
|
17
|
+
export type CreateJwtParams = Readonly<{
|
|
18
|
+
/**
|
|
19
|
+
* The keys required to sign and encrypt the JWT.
|
|
20
|
+
*
|
|
21
|
+
* These keys should be kept secret and never shared with any frontend, client, etc.
|
|
22
|
+
*/
|
|
23
|
+
jwtKeys: Readonly<JwtKeys>;
|
|
24
|
+
/**
|
|
25
|
+
* The name of the company, the name of the service, or the URL to the service that originally
|
|
26
|
+
* issued the JWT. The same value must be used when creating and parsing a JWT or the parse will
|
|
27
|
+
* fail.
|
|
28
|
+
*
|
|
29
|
+
* This name can be anything you want.
|
|
30
|
+
*
|
|
31
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
|
|
32
|
+
*/
|
|
33
|
+
issuer: string;
|
|
34
|
+
/**
|
|
35
|
+
* The arbitrary name or URL of the client intended to consume the JWT. The host and client must
|
|
36
|
+
* both know this name in order for the token to be signed and read correctly.
|
|
37
|
+
*
|
|
38
|
+
* This name can be anything you want.
|
|
39
|
+
*
|
|
40
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3
|
|
41
|
+
*/
|
|
42
|
+
audience: string;
|
|
43
|
+
/**
|
|
44
|
+
* The duration until the JWT expires.
|
|
45
|
+
*
|
|
46
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
|
|
47
|
+
*/
|
|
48
|
+
jwtDuration: Readonly<AnyDuration>;
|
|
49
|
+
}> & Readonly<PartialWithUndefined<{
|
|
50
|
+
/**
|
|
51
|
+
* Set a custom issued at date.
|
|
52
|
+
*
|
|
53
|
+
* This should usually not be overridden.
|
|
54
|
+
*
|
|
55
|
+
* @default Date.now()
|
|
56
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6
|
|
57
|
+
*/
|
|
58
|
+
issuedAt: DateLike;
|
|
59
|
+
/**
|
|
60
|
+
* Set a custom date for when the JWT will become valid. The JWT will be considered
|
|
61
|
+
* invalid and not be processed until this date.
|
|
62
|
+
*
|
|
63
|
+
* This should usually not be overridden.
|
|
64
|
+
*
|
|
65
|
+
* @default
|
|
66
|
+
* none, the JWT will be immediately valid
|
|
67
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
|
|
68
|
+
*/
|
|
69
|
+
notValidUntil: DateLike;
|
|
70
|
+
}>>;
|
|
71
|
+
/**
|
|
72
|
+
* JWT uses seconds since the epoch per RFC 7519, whereas `toTimestamp` uses milliseconds.
|
|
73
|
+
*
|
|
74
|
+
* @category Internal
|
|
75
|
+
*/
|
|
76
|
+
export declare function toJwtTimestamp(date: Readonly<FullDate>): number;
|
|
77
|
+
/**
|
|
78
|
+
* Converts a JWT timestamp (in seconds) into a FullDate instance.
|
|
79
|
+
*
|
|
80
|
+
* @category Internal
|
|
81
|
+
*/
|
|
82
|
+
export declare function parseJwtTimestamp(seconds: number): FullDate<UtcTimezone>;
|
|
83
|
+
/**
|
|
84
|
+
* Creates a signed and encrypted JWT that contains the given data.
|
|
85
|
+
*
|
|
86
|
+
* @category Internal
|
|
87
|
+
*/
|
|
88
|
+
export declare function createJwt<JwtData extends AnyObject = AnyObject>(
|
|
89
|
+
/** The data to be included in the JWT. */
|
|
90
|
+
data: JwtData, params: Readonly<CreateJwtParams>): Promise<string>;
|
|
91
|
+
/**
|
|
92
|
+
* Params for {@link parseJwt}.
|
|
93
|
+
*
|
|
94
|
+
* @category Internal
|
|
95
|
+
*/
|
|
96
|
+
export type ParseJwtParams = Readonly<SelectFrom<CreateJwtParams, {
|
|
97
|
+
issuer: true;
|
|
98
|
+
audience: true;
|
|
99
|
+
jwtKeys: true;
|
|
100
|
+
}>> & PartialWithUndefined<{
|
|
101
|
+
/**
|
|
102
|
+
* Allowed clock skew tolerance for JWT expiration and timestamp checks. Accounts for
|
|
103
|
+
* differences between server and client clocks.
|
|
104
|
+
*
|
|
105
|
+
* @default {minutes: 5}
|
|
106
|
+
*/
|
|
107
|
+
allowedClockSkew: Readonly<AnyDuration>;
|
|
108
|
+
}>;
|
|
109
|
+
/**
|
|
110
|
+
* A fully parsed JWT with embedded data.
|
|
111
|
+
*
|
|
112
|
+
* @category Internal
|
|
113
|
+
*/
|
|
114
|
+
export type ParsedJwt<JwtData extends AnyObject> = {
|
|
115
|
+
data: JwtData;
|
|
116
|
+
jwtExpiration: FullDate<UtcTimezone>;
|
|
117
|
+
/** When the JWT was issued (`iat` claim). */
|
|
118
|
+
jwtIssuedAt: FullDate<UtcTimezone>;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Parse and extract all data from an encrypted and signed JWT.
|
|
122
|
+
*
|
|
123
|
+
* @category Internal
|
|
124
|
+
* @throws Errors if the decryption, signature verification, or other JWT requirements fail
|
|
125
|
+
*/
|
|
126
|
+
export declare function parseJwt<JwtData extends AnyObject = AnyObject>(encryptedJwt: string, params: Readonly<ParseJwtParams>): Promise<ParsedJwt<JwtData>>;
|
package/dist/jwt/jwt.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { assertWrap, check } from '@augment-vir/assert';
|
|
2
|
+
import { calculateRelativeDate, convertDuration, createFullDateInUserTimezone, createUtcFullDate, getNowInUtcTimezone, toTimestamp, } from 'date-vir';
|
|
3
|
+
import { EncryptJWT, jwtDecrypt, jwtVerify, SignJWT } from 'jose';
|
|
4
|
+
const encryptionProtectedHeader = {
|
|
5
|
+
alg: 'dir',
|
|
6
|
+
enc: 'A256GCM',
|
|
7
|
+
};
|
|
8
|
+
const signingProtectedHeader = {
|
|
9
|
+
alg: 'HS512',
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Default allowed clock skew for JWT expiration checks. Accounts for differences between server and
|
|
13
|
+
* client clocks.
|
|
14
|
+
*
|
|
15
|
+
* @category Internal
|
|
16
|
+
* @default {minutes: 5}
|
|
17
|
+
*/
|
|
18
|
+
export const defaultAllowedClockSkew = {
|
|
19
|
+
minutes: 5,
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* JWT uses seconds since the epoch per RFC 7519, whereas `toTimestamp` uses milliseconds.
|
|
23
|
+
*
|
|
24
|
+
* @category Internal
|
|
25
|
+
*/
|
|
26
|
+
export function toJwtTimestamp(date) {
|
|
27
|
+
return Math.floor(toTimestamp(date) / 1000);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Converts a JWT timestamp (in seconds) into a FullDate instance.
|
|
31
|
+
*
|
|
32
|
+
* @category Internal
|
|
33
|
+
*/
|
|
34
|
+
export function parseJwtTimestamp(seconds) {
|
|
35
|
+
return createUtcFullDate(seconds * 1000);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Creates a signed and encrypted JWT that contains the given data.
|
|
39
|
+
*
|
|
40
|
+
* @category Internal
|
|
41
|
+
*/
|
|
42
|
+
export async function createJwt(
|
|
43
|
+
/** The data to be included in the JWT. */
|
|
44
|
+
data, params) {
|
|
45
|
+
const rawJwt = new SignJWT({
|
|
46
|
+
data,
|
|
47
|
+
})
|
|
48
|
+
.setProtectedHeader(signingProtectedHeader)
|
|
49
|
+
.setIssuedAt(params.issuedAt
|
|
50
|
+
? toJwtTimestamp(createFullDateInUserTimezone(params.issuedAt))
|
|
51
|
+
: undefined)
|
|
52
|
+
.setIssuer(params.issuer)
|
|
53
|
+
.setAudience(params.audience)
|
|
54
|
+
.setExpirationTime(toJwtTimestamp(calculateRelativeDate(getNowInUtcTimezone(), params.jwtDuration)));
|
|
55
|
+
if (params.notValidUntil) {
|
|
56
|
+
rawJwt.setNotBefore(toJwtTimestamp(createFullDateInUserTimezone(params.notValidUntil)));
|
|
57
|
+
}
|
|
58
|
+
const signedJwt = await rawJwt.sign(params.jwtKeys.signingKey);
|
|
59
|
+
return await new EncryptJWT({
|
|
60
|
+
jwt: signedJwt,
|
|
61
|
+
})
|
|
62
|
+
.setProtectedHeader(encryptionProtectedHeader)
|
|
63
|
+
.encrypt(params.jwtKeys.encryptionKey);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Parse and extract all data from an encrypted and signed JWT.
|
|
67
|
+
*
|
|
68
|
+
* @category Internal
|
|
69
|
+
* @throws Errors if the decryption, signature verification, or other JWT requirements fail
|
|
70
|
+
*/
|
|
71
|
+
export async function parseJwt(encryptedJwt, params) {
|
|
72
|
+
const decryptedJwt = await jwtDecrypt(encryptedJwt, params.jwtKeys.encryptionKey);
|
|
73
|
+
if (!check.deepEquals(decryptedJwt.protectedHeader, encryptionProtectedHeader)) {
|
|
74
|
+
throw new Error('Invalid encryption protected header.');
|
|
75
|
+
}
|
|
76
|
+
else if (!check.isString(decryptedJwt.payload.jwt)) {
|
|
77
|
+
throw new TypeError('Decrypted jwt is not a string.');
|
|
78
|
+
}
|
|
79
|
+
const clockToleranceSeconds = convertDuration(params.allowedClockSkew || defaultAllowedClockSkew, {
|
|
80
|
+
seconds: true,
|
|
81
|
+
}).seconds;
|
|
82
|
+
const verifiedJwt = await jwtVerify(decryptedJwt.payload.jwt, params.jwtKeys.signingKey, {
|
|
83
|
+
issuer: params.issuer,
|
|
84
|
+
audience: params.audience,
|
|
85
|
+
requiredClaims: [
|
|
86
|
+
'iat',
|
|
87
|
+
'aud',
|
|
88
|
+
'iss',
|
|
89
|
+
],
|
|
90
|
+
clockTolerance: clockToleranceSeconds,
|
|
91
|
+
});
|
|
92
|
+
if (!verifiedJwt.payload.iat ||
|
|
93
|
+
verifiedJwt.payload.iat * 1000 > Date.now() + clockToleranceSeconds * 1000) {
|
|
94
|
+
throw new Error('"iat" claim timestamp check failed');
|
|
95
|
+
}
|
|
96
|
+
const data = verifiedJwt.payload.data;
|
|
97
|
+
if (!check.deepEquals(verifiedJwt.protectedHeader, signingProtectedHeader)) {
|
|
98
|
+
throw new Error('Invalid signing protected header.');
|
|
99
|
+
}
|
|
100
|
+
const issuedAtSeconds = assertWrap.isDefined(verifiedJwt.payload.iat, 'JWT has no issued at.');
|
|
101
|
+
const expirationSeconds = assertWrap.isDefined(verifiedJwt.payload.exp, 'JWT has no expiration.');
|
|
102
|
+
const jwtIssuedAt = parseJwtTimestamp(issuedAtSeconds);
|
|
103
|
+
const jwtExpiration = parseJwtTimestamp(expirationSeconds);
|
|
104
|
+
return {
|
|
105
|
+
data: data,
|
|
106
|
+
jwtExpiration,
|
|
107
|
+
jwtIssuedAt,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type CreateJwtParams, type ParsedJwt, type ParseJwtParams } from './jwt.js';
|
|
2
|
+
/**
|
|
3
|
+
* Shape definition and source of truth for {@link JwtUserData}.
|
|
4
|
+
*
|
|
5
|
+
* @category Internal
|
|
6
|
+
*/
|
|
7
|
+
export declare const userJwtDataShape: import("object-shape-tester").Shape<{
|
|
8
|
+
/** The id from your database of the user you're authenticating. */
|
|
9
|
+
userId: import("object-shape-tester").Shape<import("@sinclair/typebox").TUnion<(import("@sinclair/typebox").TString | import("@sinclair/typebox").TNumber)[]>>;
|
|
10
|
+
/**
|
|
11
|
+
* CSRF token. This can be any cryptographically secure randomized string.
|
|
12
|
+
*
|
|
13
|
+
* Consider using {@link generateCsrfToken} to generate this.
|
|
14
|
+
*/
|
|
15
|
+
csrfToken: string;
|
|
16
|
+
/**
|
|
17
|
+
* Unix timestamp (in milliseconds) when the session was originally started. This is used to
|
|
18
|
+
* enforce the max session duration. If not present, the session is considered to have started
|
|
19
|
+
* when the JWT was issued.
|
|
20
|
+
*/
|
|
21
|
+
sessionStartedAt: import("object-shape-tester").Shape<import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TUndefined, import("@sinclair/typebox").TNumber]>>>;
|
|
22
|
+
}>;
|
|
23
|
+
/**
|
|
24
|
+
* Data required for user JWTs.
|
|
25
|
+
*
|
|
26
|
+
* @category Internal
|
|
27
|
+
*/
|
|
28
|
+
export type JwtUserData = typeof userJwtDataShape.runtimeType;
|
|
29
|
+
/**
|
|
30
|
+
* Creates a new signed and encrypted {@link JwtUserData} when a client (frontend) successfully
|
|
31
|
+
* authenticates with the host (backend). This is used by host (backend) code to establish a new
|
|
32
|
+
* user session. The output of this function should be sent to the client (frontend) for storage.
|
|
33
|
+
*
|
|
34
|
+
* @category Internal
|
|
35
|
+
*/
|
|
36
|
+
export declare function createUserJwt(data: Readonly<JwtUserData>, params: Readonly<CreateJwtParams>): Promise<string>;
|
|
37
|
+
/**
|
|
38
|
+
* Parses a {@link JwtUserData} generated from {@link createUserJwt}. This should be used on the host
|
|
39
|
+
* (backend) to a client (frontend) request. Do not use this function in client (frontend) code: it
|
|
40
|
+
* requires JWT signing keys which should not be shared with any client (frontend).
|
|
41
|
+
*
|
|
42
|
+
* @category Internal
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseUserJwt(encryptedJwt: string, params: Readonly<ParseJwtParams>): Promise<ParsedJwt<JwtUserData> | undefined>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { checkValidShape, defineShape, optionalShape, unionShape } from 'object-shape-tester';
|
|
2
|
+
import { createJwt, parseJwt, } from './jwt.js';
|
|
3
|
+
/**
|
|
4
|
+
* Shape definition and source of truth for {@link JwtUserData}.
|
|
5
|
+
*
|
|
6
|
+
* @category Internal
|
|
7
|
+
*/
|
|
8
|
+
export const userJwtDataShape = defineShape({
|
|
9
|
+
/** The id from your database of the user you're authenticating. */
|
|
10
|
+
userId: unionShape('', -1),
|
|
11
|
+
/**
|
|
12
|
+
* CSRF token. This can be any cryptographically secure randomized string.
|
|
13
|
+
*
|
|
14
|
+
* Consider using {@link generateCsrfToken} to generate this.
|
|
15
|
+
*/
|
|
16
|
+
csrfToken: '',
|
|
17
|
+
/**
|
|
18
|
+
* Unix timestamp (in milliseconds) when the session was originally started. This is used to
|
|
19
|
+
* enforce the max session duration. If not present, the session is considered to have started
|
|
20
|
+
* when the JWT was issued.
|
|
21
|
+
*/
|
|
22
|
+
sessionStartedAt: optionalShape(0, {
|
|
23
|
+
alsoUndefined: true,
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Creates a new signed and encrypted {@link JwtUserData} when a client (frontend) successfully
|
|
28
|
+
* authenticates with the host (backend). This is used by host (backend) code to establish a new
|
|
29
|
+
* user session. The output of this function should be sent to the client (frontend) for storage.
|
|
30
|
+
*
|
|
31
|
+
* @category Internal
|
|
32
|
+
*/
|
|
33
|
+
export async function createUserJwt(data, params) {
|
|
34
|
+
return await createJwt(data, params);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parses a {@link JwtUserData} generated from {@link createUserJwt}. This should be used on the host
|
|
38
|
+
* (backend) to a client (frontend) request. Do not use this function in client (frontend) code: it
|
|
39
|
+
* requires JWT signing keys which should not be shared with any client (frontend).
|
|
40
|
+
*
|
|
41
|
+
* @category Internal
|
|
42
|
+
*/
|
|
43
|
+
export async function parseUserJwt(encryptedJwt, params) {
|
|
44
|
+
const { data, jwtExpiration, jwtIssuedAt } = await parseJwt(encryptedJwt, params);
|
|
45
|
+
if (!checkValidShape(data, userJwtDataShape)) {
|
|
46
|
+
throw new TypeError('Verified jwt has wrong data.');
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
data,
|
|
50
|
+
jwtExpiration,
|
|
51
|
+
jwtIssuedAt,
|
|
52
|
+
};
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auth-vir",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.4",
|
|
4
4
|
"description": "Auth made easy and secure via JWT cookies, CSRF tokens, and password hashing helpers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"url": "https://github.com/electrovir"
|
|
26
26
|
},
|
|
27
27
|
"type": "module",
|
|
28
|
-
"main": "
|
|
29
|
-
"module": "
|
|
30
|
-
"types": "
|
|
28
|
+
"main": "dist/index.js",
|
|
29
|
+
"module": "dist/index.js",
|
|
30
|
+
"types": "dist/index.d.ts",
|
|
31
31
|
"bin": "bin.js",
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "virmator frontend build",
|