auth-vir 0.0.0 → 0.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/LICENSE-CC0 +121 -0
- package/LICENSE-MIT +21 -0
- package/README.md +322 -0
- package/bin.js +8 -0
- package/dist/auth.d.ts +79 -0
- package/dist/auth.js +101 -0
- package/dist/cookie.d.ts +50 -0
- package/dist/cookie.js +38 -0
- package/dist/csrf-token.d.ts +12 -0
- package/dist/csrf-token.js +15 -0
- package/dist/hash.d.ts +35 -0
- package/dist/hash.js +51 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/jwt-keys.d.ts +44 -0
- package/dist/jwt-keys.js +57 -0
- package/dist/jwt-keys.script.d.ts +1 -0
- package/dist/jwt-keys.script.js +2 -0
- package/dist/jwt.d.ts +83 -0
- package/dist/jwt.js +61 -0
- package/dist/mock-local-storage.d.ts +33 -0
- package/dist/mock-local-storage.js +56 -0
- package/dist/user-jwt.d.ts +38 -0
- package/dist/user-jwt.js +41 -0
- package/package.json +66 -11
- package/src/auth.ts +168 -0
- package/src/cookie.ts +85 -0
- package/src/csrf-token.ts +17 -0
- package/src/hash.ts +65 -0
- package/src/index.ts +8 -0
- package/src/jwt-keys.script.ts +3 -0
- package/src/jwt-keys.ts +111 -0
- package/src/jwt.ts +160 -0
- package/src/mock-local-storage.ts +72 -0
- package/src/user-jwt.ts +60 -0
package/dist/cookie.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type PartialWithUndefined } from '@augment-vir/common';
|
|
2
|
+
import { type AnyDuration } from 'date-vir';
|
|
3
|
+
import { CreateJwtParams, type ParseJwtParams } from './jwt.js';
|
|
4
|
+
import { UserJwtData } from './user-jwt.js';
|
|
5
|
+
/**
|
|
6
|
+
* Parameters for {@link generateCookie}.
|
|
7
|
+
*
|
|
8
|
+
* @category Internal
|
|
9
|
+
*/
|
|
10
|
+
export type CookieParams = {
|
|
11
|
+
/**
|
|
12
|
+
* The origin of the host (backend) service that cookies will be included in all requests to.
|
|
13
|
+
* This should be restricted to just your host (backend) origin for security purposes.
|
|
14
|
+
*
|
|
15
|
+
* @example 'https://www.example.com'
|
|
16
|
+
*/
|
|
17
|
+
hostOrigin: string;
|
|
18
|
+
/**
|
|
19
|
+
* The max duration of this cookie. Or, in other words, the max user session duration before
|
|
20
|
+
* they're logged out.
|
|
21
|
+
*/
|
|
22
|
+
cookieDuration: AnyDuration;
|
|
23
|
+
/**
|
|
24
|
+
* All JWT parameters required for generating the encrypted JWT that will be embedded in the
|
|
25
|
+
* Cookie. Note that all JWT keys contained herein should never shared with any frontend,
|
|
26
|
+
* client, etc.
|
|
27
|
+
*/
|
|
28
|
+
jwtParams: Readonly<CreateJwtParams>;
|
|
29
|
+
} & PartialWithUndefined<{
|
|
30
|
+
/**
|
|
31
|
+
* Is set to `true` (which should only be done in development environments), the cookie will be
|
|
32
|
+
* allowed in insecure requests (non HTTPS requests).
|
|
33
|
+
*
|
|
34
|
+
* @default false
|
|
35
|
+
*/
|
|
36
|
+
isDev: boolean;
|
|
37
|
+
}>;
|
|
38
|
+
/**
|
|
39
|
+
* Generate a secure cookie that stores the user JWT data. Used in host (backend) code.
|
|
40
|
+
*
|
|
41
|
+
* @category Internal
|
|
42
|
+
*/
|
|
43
|
+
export declare function generateCookie(userJwtData: Readonly<UserJwtData>, cookieConfig: Readonly<CookieParams>): Promise<string>;
|
|
44
|
+
/**
|
|
45
|
+
* Extract an auth cookie from a cookie string. Used in host (backend) code.
|
|
46
|
+
*
|
|
47
|
+
* @category Internal
|
|
48
|
+
* @returns The extracted auth Cookie JWT data or `undefined` if no valid auth JWT data was found.
|
|
49
|
+
*/
|
|
50
|
+
export declare function extractCookieJwt(rawCookie: string, jwtParams: Readonly<ParseJwtParams>): Promise<undefined | UserJwtData>;
|
package/dist/cookie.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { check } from '@augment-vir/assert';
|
|
2
|
+
import { safeMatch } from '@augment-vir/common';
|
|
3
|
+
import { convertDuration } from 'date-vir';
|
|
4
|
+
import { parseUrl } from 'url-vir';
|
|
5
|
+
import { createUserJwt, parseUserJwt } from './user-jwt.js';
|
|
6
|
+
/**
|
|
7
|
+
* Generate a secure cookie that stores the user JWT data. Used in host (backend) code.
|
|
8
|
+
*
|
|
9
|
+
* @category Internal
|
|
10
|
+
*/
|
|
11
|
+
export async function generateCookie(userJwtData, cookieConfig) {
|
|
12
|
+
return [
|
|
13
|
+
`auth=${await createUserJwt(userJwtData, cookieConfig.jwtParams)}`,
|
|
14
|
+
`Domain=${parseUrl(cookieConfig.hostOrigin).hostname}`,
|
|
15
|
+
'HttpOnly',
|
|
16
|
+
'Path=/',
|
|
17
|
+
'SameSite=Strict',
|
|
18
|
+
`MAX-AGE=${convertDuration(cookieConfig.cookieDuration, { seconds: true }).seconds}`,
|
|
19
|
+
cookieConfig.isDev ? '' : 'Secure',
|
|
20
|
+
]
|
|
21
|
+
.filter(check.isTruthy)
|
|
22
|
+
.join('; ');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Extract an auth cookie from a cookie string. Used in host (backend) code.
|
|
26
|
+
*
|
|
27
|
+
* @category Internal
|
|
28
|
+
* @returns The extracted auth Cookie JWT data or `undefined` if no valid auth JWT data was found.
|
|
29
|
+
*/
|
|
30
|
+
export async function extractCookieJwt(rawCookie, jwtParams) {
|
|
31
|
+
const [auth] = safeMatch(rawCookie, /auth=[^;]+(?:;|$)/);
|
|
32
|
+
if (!auth) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const rawJwt = auth.replace('auth=', '').replace(';', '');
|
|
36
|
+
const jwt = await parseUserJwt(rawJwt, jwtParams);
|
|
37
|
+
return jwt;
|
|
38
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Name of the header attached to responses that stores the CSRF token value.
|
|
3
|
+
*
|
|
4
|
+
* @category Internal
|
|
5
|
+
*/
|
|
6
|
+
export declare const csrfTokenHeaderName = "csrf-token";
|
|
7
|
+
/**
|
|
8
|
+
* Generates a random, cryptographically secure CSRF token.
|
|
9
|
+
*
|
|
10
|
+
* @category Internal
|
|
11
|
+
*/
|
|
12
|
+
export declare function generateCsrfToken(): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { randomString } from '@augment-vir/common';
|
|
2
|
+
/**
|
|
3
|
+
* Name of the header attached to responses that stores the CSRF token value.
|
|
4
|
+
*
|
|
5
|
+
* @category Internal
|
|
6
|
+
*/
|
|
7
|
+
export const csrfTokenHeaderName = 'csrf-token';
|
|
8
|
+
/**
|
|
9
|
+
* Generates a random, cryptographically secure CSRF token.
|
|
10
|
+
*
|
|
11
|
+
* @category Internal
|
|
12
|
+
*/
|
|
13
|
+
export function generateCsrfToken() {
|
|
14
|
+
return randomString(256);
|
|
15
|
+
}
|
package/dist/hash.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hashes a password using the bcrypt algorithm so passwords don't need to be stored in plain text.
|
|
3
|
+
* The output of this function is safe to store in a database for future credential comparisons.
|
|
4
|
+
*
|
|
5
|
+
* @category Auth : Host
|
|
6
|
+
* @returns `undefined` if the password is too long (and would be truncated by the bcrypt hashing
|
|
7
|
+
* algorithm). Otherwise, the hashed output.
|
|
8
|
+
* @see https://wikipedia.org/wiki/Bcrypt
|
|
9
|
+
*/
|
|
10
|
+
export declare function hashPassword(password: string): Promise<undefined | string>;
|
|
11
|
+
/**
|
|
12
|
+
* Checks if the given string will be truncated when passed through {@link hashPassword}. Passwords
|
|
13
|
+
* longer than this should not be accepted.
|
|
14
|
+
*
|
|
15
|
+
* @category Internal
|
|
16
|
+
*/
|
|
17
|
+
export declare function willHashTruncate(input: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* A utility that provides more accurate string byte size than doing `string.length`.
|
|
20
|
+
*
|
|
21
|
+
* @category Internal
|
|
22
|
+
*/
|
|
23
|
+
export declare function getByteLength(input: string): number;
|
|
24
|
+
/**
|
|
25
|
+
* Checks if the given password is a match by comparing it to its previously computed and stored
|
|
26
|
+
* hash.
|
|
27
|
+
*
|
|
28
|
+
* @category Auth : Host
|
|
29
|
+
*/
|
|
30
|
+
export declare function doesPasswordMatchHash({ password, hash, }: {
|
|
31
|
+
/** The password entered by the user in their login attempt. */
|
|
32
|
+
password: string;
|
|
33
|
+
/** The stored password hash for that user. */
|
|
34
|
+
hash: string;
|
|
35
|
+
}): Promise<boolean>;
|
package/dist/hash.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { bcrypt, bcryptVerify } from 'hash-wasm';
|
|
2
|
+
/**
|
|
3
|
+
* Hashes a password using the bcrypt algorithm so passwords don't need to be stored in plain text.
|
|
4
|
+
* The output of this function is safe to store in a database for future credential comparisons.
|
|
5
|
+
*
|
|
6
|
+
* @category Auth : Host
|
|
7
|
+
* @returns `undefined` if the password is too long (and would be truncated by the bcrypt hashing
|
|
8
|
+
* algorithm). Otherwise, the hashed output.
|
|
9
|
+
* @see https://wikipedia.org/wiki/Bcrypt
|
|
10
|
+
*/
|
|
11
|
+
export async function hashPassword(password) {
|
|
12
|
+
if (willHashTruncate(password)) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const salt = new Uint8Array(16);
|
|
16
|
+
globalThis.crypto.getRandomValues(salt);
|
|
17
|
+
return await bcrypt({
|
|
18
|
+
costFactor: 10,
|
|
19
|
+
password: password.normalize(),
|
|
20
|
+
salt,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Checks if the given string will be truncated when passed through {@link hashPassword}. Passwords
|
|
25
|
+
* longer than this should not be accepted.
|
|
26
|
+
*
|
|
27
|
+
* @category Internal
|
|
28
|
+
*/
|
|
29
|
+
export function willHashTruncate(input) {
|
|
30
|
+
return getByteLength(input) > 72;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A utility that provides more accurate string byte size than doing `string.length`.
|
|
34
|
+
*
|
|
35
|
+
* @category Internal
|
|
36
|
+
*/
|
|
37
|
+
export function getByteLength(input) {
|
|
38
|
+
return new Blob([input]).size;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Checks if the given password is a match by comparing it to its previously computed and stored
|
|
42
|
+
* hash.
|
|
43
|
+
*
|
|
44
|
+
* @category Auth : Host
|
|
45
|
+
*/
|
|
46
|
+
export async function doesPasswordMatchHash({ password, hash, }) {
|
|
47
|
+
return await bcryptVerify({
|
|
48
|
+
hash,
|
|
49
|
+
password,
|
|
50
|
+
});
|
|
51
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -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>>;
|
package/dist/jwt-keys.js
ADDED
|
@@ -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 {};
|
package/dist/jwt.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { AnyObject, PartialWithUndefined } from '@augment-vir/common';
|
|
2
|
+
import { type AnyDuration, type DateLike } from 'date-vir';
|
|
3
|
+
import { JwtKeys } from './jwt-keys.js';
|
|
4
|
+
/**
|
|
5
|
+
* Params for {@link createJwt}.
|
|
6
|
+
*
|
|
7
|
+
* @category Internal
|
|
8
|
+
*/
|
|
9
|
+
export type CreateJwtParams = Readonly<{
|
|
10
|
+
/**
|
|
11
|
+
* The keys required to sign and encrypt the JWT.
|
|
12
|
+
*
|
|
13
|
+
* These keys should be kept secret and never shared with any frontend, client, etc.
|
|
14
|
+
*/
|
|
15
|
+
jwtKeys: Readonly<JwtKeys>;
|
|
16
|
+
/**
|
|
17
|
+
* The name of the company, the name of the service, or the URL to the service that originally
|
|
18
|
+
* issued the JWT. The same value must be used when creating and parsing a JWT or the parse will
|
|
19
|
+
* fail.
|
|
20
|
+
*
|
|
21
|
+
* This name can be anything you want.
|
|
22
|
+
*
|
|
23
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
|
|
24
|
+
*/
|
|
25
|
+
issuer: string;
|
|
26
|
+
/**
|
|
27
|
+
* The arbitrary name or URL of the client intended to consume the JWT. The host and client must
|
|
28
|
+
* both know this name in order for the token to be signed and read correctly.
|
|
29
|
+
*
|
|
30
|
+
* This name can be anything you want.
|
|
31
|
+
*
|
|
32
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3
|
|
33
|
+
*/
|
|
34
|
+
audience: string;
|
|
35
|
+
/**
|
|
36
|
+
* The duration until the JWT expires.
|
|
37
|
+
*
|
|
38
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
|
|
39
|
+
*/
|
|
40
|
+
jwtDuration: Readonly<AnyDuration>;
|
|
41
|
+
}> & Readonly<PartialWithUndefined<{
|
|
42
|
+
/**
|
|
43
|
+
* Set a custom issued at date.
|
|
44
|
+
*
|
|
45
|
+
* This should usually not be overridden.
|
|
46
|
+
*
|
|
47
|
+
* @default Date.now()
|
|
48
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6
|
|
49
|
+
*/
|
|
50
|
+
issuedAt: DateLike;
|
|
51
|
+
/**
|
|
52
|
+
* Set a custom date for when the JWT will become valid. The JWT will be considered
|
|
53
|
+
* invalid and not be processed until this date.
|
|
54
|
+
*
|
|
55
|
+
* This should usually not be overridden.
|
|
56
|
+
*
|
|
57
|
+
* @default
|
|
58
|
+
* none, the JWT will be immediately valid
|
|
59
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
|
|
60
|
+
*/
|
|
61
|
+
notValidUntil: DateLike;
|
|
62
|
+
}>>;
|
|
63
|
+
/**
|
|
64
|
+
* Creates a signed and encrypted JWT that contains the given data.
|
|
65
|
+
*
|
|
66
|
+
* @category Internal
|
|
67
|
+
*/
|
|
68
|
+
export declare function createJwt<JwtData extends AnyObject = AnyObject>(
|
|
69
|
+
/** The data to be included in the JWT. */
|
|
70
|
+
data: JwtData, params: Readonly<CreateJwtParams>): Promise<string>;
|
|
71
|
+
/**
|
|
72
|
+
* Params for {@link parseJwt}.
|
|
73
|
+
*
|
|
74
|
+
* @category Internal
|
|
75
|
+
*/
|
|
76
|
+
export type ParseJwtParams = Readonly<Pick<CreateJwtParams, 'issuer' | 'audience' | 'jwtKeys'>>;
|
|
77
|
+
/**
|
|
78
|
+
* Parse and extract all data from an encrypted and signed JWT.
|
|
79
|
+
*
|
|
80
|
+
* @category Internal
|
|
81
|
+
* @throws Errors if the decryption, signature verification, or other JWT requirements fail
|
|
82
|
+
*/
|
|
83
|
+
export declare function parseJwt<JwtData extends AnyObject = AnyObject>(encryptedJwt: string, params: Readonly<ParseJwtParams>): Promise<JwtData>;
|
package/dist/jwt.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { check } from '@augment-vir/assert';
|
|
2
|
+
import { calculateRelativeDate, createFullDateInUserTimezone, getNowInUtcTimezone, toTimestamp, } from 'date-vir';
|
|
3
|
+
import { EncryptJWT, jwtDecrypt, jwtVerify, SignJWT } from 'jose';
|
|
4
|
+
const encryptionProtectedHeader = { alg: 'dir', enc: 'A256GCM' };
|
|
5
|
+
const signingProtectedHeader = { alg: 'HS512' };
|
|
6
|
+
/**
|
|
7
|
+
* Creates a signed and encrypted JWT that contains the given data.
|
|
8
|
+
*
|
|
9
|
+
* @category Internal
|
|
10
|
+
*/
|
|
11
|
+
export async function createJwt(
|
|
12
|
+
/** The data to be included in the JWT. */
|
|
13
|
+
data, params) {
|
|
14
|
+
const rawJwt = new SignJWT({ data: data })
|
|
15
|
+
.setProtectedHeader(signingProtectedHeader)
|
|
16
|
+
.setIssuedAt(params.issuedAt
|
|
17
|
+
? toTimestamp(createFullDateInUserTimezone(params.issuedAt))
|
|
18
|
+
: undefined)
|
|
19
|
+
.setIssuer(params.issuer)
|
|
20
|
+
.setAudience(params.audience)
|
|
21
|
+
.setExpirationTime(toTimestamp(calculateRelativeDate(getNowInUtcTimezone(), params.jwtDuration)));
|
|
22
|
+
if (params.notValidUntil) {
|
|
23
|
+
rawJwt.setNotBefore(toTimestamp(createFullDateInUserTimezone(params.notValidUntil)));
|
|
24
|
+
}
|
|
25
|
+
const signedJwt = await rawJwt.sign(params.jwtKeys.signingKey);
|
|
26
|
+
return await new EncryptJWT({ jwt: signedJwt })
|
|
27
|
+
.setProtectedHeader(encryptionProtectedHeader)
|
|
28
|
+
.encrypt(params.jwtKeys.encryptionKey);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse and extract all data from an encrypted and signed JWT.
|
|
32
|
+
*
|
|
33
|
+
* @category Internal
|
|
34
|
+
* @throws Errors if the decryption, signature verification, or other JWT requirements fail
|
|
35
|
+
*/
|
|
36
|
+
export async function parseJwt(encryptedJwt, params) {
|
|
37
|
+
const decryptedJwt = await jwtDecrypt(encryptedJwt, params.jwtKeys.encryptionKey);
|
|
38
|
+
if (!check.deepEquals(decryptedJwt.protectedHeader, encryptionProtectedHeader)) {
|
|
39
|
+
throw new Error('Invalid encryption protected header.');
|
|
40
|
+
}
|
|
41
|
+
else if (!check.isString(decryptedJwt.payload.jwt)) {
|
|
42
|
+
throw new TypeError('Decrypted jwt is not a string.');
|
|
43
|
+
}
|
|
44
|
+
const verifiedJwt = await jwtVerify(decryptedJwt.payload.jwt, params.jwtKeys.signingKey, {
|
|
45
|
+
issuer: params.issuer,
|
|
46
|
+
audience: params.audience,
|
|
47
|
+
requiredClaims: [
|
|
48
|
+
'iat',
|
|
49
|
+
'aud',
|
|
50
|
+
'iss',
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
if (!verifiedJwt.payload.iat || verifiedJwt.payload.iat * 1000 > Date.now()) {
|
|
54
|
+
throw new Error('"iat" claim timestamp check failed');
|
|
55
|
+
}
|
|
56
|
+
const data = verifiedJwt.payload.data;
|
|
57
|
+
if (!check.deepEquals(verifiedJwt.protectedHeader, signingProtectedHeader)) {
|
|
58
|
+
throw new Error('Invalid signing protected header.');
|
|
59
|
+
}
|
|
60
|
+
return data;
|
|
61
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `accessRecord` type for {@link createMockLocalStorage}'s output.
|
|
3
|
+
*
|
|
4
|
+
* @category Internal
|
|
5
|
+
*/
|
|
6
|
+
export type MockLocalStorageAccessRecord = {
|
|
7
|
+
getItem: string[];
|
|
8
|
+
removeItem: string[];
|
|
9
|
+
setItem: {
|
|
10
|
+
key: string;
|
|
11
|
+
value: string;
|
|
12
|
+
}[];
|
|
13
|
+
key: number[];
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Create an empty `accessRecord` object, this is to be used in conjunction with
|
|
17
|
+
* {@link createMockLocalStorage}.
|
|
18
|
+
*
|
|
19
|
+
* @category Mock
|
|
20
|
+
*/
|
|
21
|
+
export declare function createEmptyMockLocalStorageAccessRecord(): MockLocalStorageAccessRecord;
|
|
22
|
+
/**
|
|
23
|
+
* Create a LocalStorage mock.
|
|
24
|
+
*
|
|
25
|
+
* @category Mock
|
|
26
|
+
*/
|
|
27
|
+
export declare function createMockLocalStorage(
|
|
28
|
+
/** Set values in here to initialize the mocked localStorage data store contents. */
|
|
29
|
+
init?: Record<string, string>): {
|
|
30
|
+
localStorage: Storage;
|
|
31
|
+
store: Record<string, string>;
|
|
32
|
+
accessRecord: MockLocalStorageAccessRecord;
|
|
33
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create an empty `accessRecord` object, this is to be used in conjunction with
|
|
3
|
+
* {@link createMockLocalStorage}.
|
|
4
|
+
*
|
|
5
|
+
* @category Mock
|
|
6
|
+
*/
|
|
7
|
+
export function createEmptyMockLocalStorageAccessRecord() {
|
|
8
|
+
return {
|
|
9
|
+
getItem: [],
|
|
10
|
+
removeItem: [],
|
|
11
|
+
setItem: [],
|
|
12
|
+
key: [],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a LocalStorage mock.
|
|
17
|
+
*
|
|
18
|
+
* @category Mock
|
|
19
|
+
*/
|
|
20
|
+
export function createMockLocalStorage(
|
|
21
|
+
/** Set values in here to initialize the mocked localStorage data store contents. */
|
|
22
|
+
init = {}) {
|
|
23
|
+
const store = init;
|
|
24
|
+
const accessRecord = createEmptyMockLocalStorageAccessRecord();
|
|
25
|
+
const mockLocalStorage = {
|
|
26
|
+
clear() {
|
|
27
|
+
Object.keys(store).forEach((key) => {
|
|
28
|
+
delete store[key];
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
getItem(key) {
|
|
32
|
+
accessRecord.getItem.push(key);
|
|
33
|
+
return store[key] ?? null;
|
|
34
|
+
},
|
|
35
|
+
get length() {
|
|
36
|
+
return Object.keys(store).length;
|
|
37
|
+
},
|
|
38
|
+
key(index) {
|
|
39
|
+
accessRecord.key.push(index);
|
|
40
|
+
return Object.keys(store)[index] ?? null;
|
|
41
|
+
},
|
|
42
|
+
removeItem(key) {
|
|
43
|
+
accessRecord.removeItem.push(key);
|
|
44
|
+
delete store[key];
|
|
45
|
+
},
|
|
46
|
+
setItem(key, value) {
|
|
47
|
+
accessRecord.setItem.push({ key, value });
|
|
48
|
+
store[key] = value;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
localStorage: mockLocalStorage,
|
|
53
|
+
store,
|
|
54
|
+
accessRecord,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { CreateJwtParams, ParseJwtParams } from './jwt.js';
|
|
2
|
+
/**
|
|
3
|
+
* Shape definition and source of truth for {@link UserJwtData}.
|
|
4
|
+
*
|
|
5
|
+
* @category Internal
|
|
6
|
+
*/
|
|
7
|
+
export declare const userJwtDataShape: import("object-shape-tester").ShapeDefinition<{
|
|
8
|
+
/** The id from your database of the user you're authenticating. */
|
|
9
|
+
userId: string;
|
|
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
|
+
}, false>;
|
|
17
|
+
/**
|
|
18
|
+
* Data required for user JWTs.
|
|
19
|
+
*
|
|
20
|
+
* @category Internal
|
|
21
|
+
*/
|
|
22
|
+
export type UserJwtData = typeof userJwtDataShape.runtimeType;
|
|
23
|
+
/**
|
|
24
|
+
* Creates a new signed and encrypted {@link UserJwtData} when a client (frontend) successfully
|
|
25
|
+
* authenticates with the host (backend). This is used by host (backend) code to establish a new
|
|
26
|
+
* user session. The output of this function should be sent to the client (frontend) for storage.
|
|
27
|
+
*
|
|
28
|
+
* @category Internal
|
|
29
|
+
*/
|
|
30
|
+
export declare function createUserJwt(data: Readonly<UserJwtData>, params: Readonly<CreateJwtParams>): Promise<string>;
|
|
31
|
+
/**
|
|
32
|
+
* Parses a {@link UserJwtData} generated from {@link createUserJwt}. This should be used on the host
|
|
33
|
+
* (backend) to a client (frontend) request. Do not use this function in client (frontend) code: it
|
|
34
|
+
* requires JWT signing keys which should not be shared with any client (frontend).
|
|
35
|
+
*
|
|
36
|
+
* @category Internal
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseUserJwt(encryptedJwt: string, params: Readonly<ParseJwtParams>): Promise<UserJwtData | undefined>;
|
package/dist/user-jwt.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { defineShape, isValidShape } from 'object-shape-tester';
|
|
2
|
+
import { createJwt, parseJwt } from './jwt.js';
|
|
3
|
+
/**
|
|
4
|
+
* Shape definition and source of truth for {@link UserJwtData}.
|
|
5
|
+
*
|
|
6
|
+
* @category Internal
|
|
7
|
+
*/
|
|
8
|
+
export const userJwtDataShape = defineShape({
|
|
9
|
+
/** The id from your database of the user you're authenticating. */
|
|
10
|
+
userId: '',
|
|
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
|
+
/**
|
|
19
|
+
* Creates a new signed and encrypted {@link UserJwtData} when a client (frontend) successfully
|
|
20
|
+
* authenticates with the host (backend). This is used by host (backend) code to establish a new
|
|
21
|
+
* user session. The output of this function should be sent to the client (frontend) for storage.
|
|
22
|
+
*
|
|
23
|
+
* @category Internal
|
|
24
|
+
*/
|
|
25
|
+
export async function createUserJwt(data, params) {
|
|
26
|
+
return await createJwt(data, params);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parses a {@link UserJwtData} generated from {@link createUserJwt}. This should be used on the host
|
|
30
|
+
* (backend) to a client (frontend) request. Do not use this function in client (frontend) code: it
|
|
31
|
+
* requires JWT signing keys which should not be shared with any client (frontend).
|
|
32
|
+
*
|
|
33
|
+
* @category Internal
|
|
34
|
+
*/
|
|
35
|
+
export async function parseUserJwt(encryptedJwt, params) {
|
|
36
|
+
const parsed = await parseJwt(encryptedJwt, params);
|
|
37
|
+
if (!isValidShape(parsed, userJwtDataShape)) {
|
|
38
|
+
throw new TypeError('Verified jwt has wrong data.');
|
|
39
|
+
}
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|