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/package.json
CHANGED
|
@@ -1,13 +1,68 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
"name": "auth-vir",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Auth made easy and secure via JWT cookies, CSRF tokens, and password hashing helpers.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"auth",
|
|
7
|
+
"jwt",
|
|
8
|
+
"csrf",
|
|
9
|
+
"hash",
|
|
10
|
+
"password",
|
|
11
|
+
"secure",
|
|
12
|
+
"cookie"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/electrovir/auth-vir",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/electrovir/auth-vir/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/electrovir/auth-vir.git"
|
|
21
|
+
},
|
|
22
|
+
"license": "(MIT or CC0 1.0)",
|
|
23
|
+
"author": {
|
|
24
|
+
"name": "electrovir",
|
|
25
|
+
"url": "https://github.com/electrovir"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "dist/index.js",
|
|
29
|
+
"module": "dist/index.js",
|
|
30
|
+
"types": "dist/index.d.ts",
|
|
31
|
+
"bin": "",
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "virmator frontend build",
|
|
34
|
+
"compile": "virmator compile",
|
|
35
|
+
"docs": "virmator docs",
|
|
36
|
+
"start": "virmator frontend",
|
|
37
|
+
"test": "virmator test web",
|
|
38
|
+
"test:coverage": "npm run test coverage",
|
|
39
|
+
"test:docs": "virmator docs check",
|
|
40
|
+
"test:update": "npm test update"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@augment-vir/assert": "^31.11.0",
|
|
44
|
+
"@augment-vir/common": "^31.11.0",
|
|
45
|
+
"date-vir": "^7.3.1",
|
|
46
|
+
"hash-wasm": "^4.12.0",
|
|
47
|
+
"jose": "^6.0.10",
|
|
48
|
+
"object-shape-tester": "^5.1.5",
|
|
49
|
+
"url-vir": "^2.1.3"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@augment-vir/test": "^31.11.0",
|
|
53
|
+
"@web/dev-server-esbuild": "^1.0.4",
|
|
54
|
+
"@web/test-runner": "^0.20.1",
|
|
55
|
+
"@web/test-runner-commands": "^0.9.0",
|
|
56
|
+
"@web/test-runner-playwright": "^0.11.0",
|
|
57
|
+
"@web/test-runner-visual-regression": "^0.10.0",
|
|
58
|
+
"istanbul-smart-text-reporter": "^1.1.5",
|
|
59
|
+
"markdown-code-example-inserter": "^3.0.3",
|
|
60
|
+
"typedoc": "^0.28.2"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=22"
|
|
64
|
+
},
|
|
65
|
+
"publishConfig": {
|
|
66
|
+
"access": "public"
|
|
67
|
+
}
|
|
13
68
|
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import {CookieParams, extractCookieJwt, generateCookie} from './cookie.js';
|
|
2
|
+
import {csrfTokenHeaderName, generateCsrfToken} from './csrf-token.js';
|
|
3
|
+
import {ParseJwtParams} from './jwt.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* All possible headers container types supported by {@link extractUserIdFromRequestHeaders}.
|
|
7
|
+
*
|
|
8
|
+
* @category Internal
|
|
9
|
+
*/
|
|
10
|
+
export type HeaderContainer = Record<string, string[] | undefined | string | number> | Headers;
|
|
11
|
+
|
|
12
|
+
function readHeader(headers: HeaderContainer, headerName: string): string | undefined {
|
|
13
|
+
if (headers instanceof Headers) {
|
|
14
|
+
return headers.get(headerName) || undefined;
|
|
15
|
+
} else {
|
|
16
|
+
const value = headers[headerName];
|
|
17
|
+
|
|
18
|
+
if (value == undefined) {
|
|
19
|
+
return undefined;
|
|
20
|
+
} else if (Array.isArray(value)) {
|
|
21
|
+
return value[0];
|
|
22
|
+
} else {
|
|
23
|
+
return String(value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Extract the user id from a request by checking both the request cookie and CSRF token. This is
|
|
30
|
+
* used by host (backend) code to help verify a request. After extracting the user id using this,
|
|
31
|
+
* you should compare it to users stored in your database.
|
|
32
|
+
*
|
|
33
|
+
* @category Auth : Host
|
|
34
|
+
* @returns The extracted user id or `undefined` if no valid auth headers exist.
|
|
35
|
+
*/
|
|
36
|
+
export async function extractUserIdFromRequestHeaders(
|
|
37
|
+
headers: HeaderContainer,
|
|
38
|
+
jwtParams: Readonly<ParseJwtParams>,
|
|
39
|
+
): Promise<string | undefined> {
|
|
40
|
+
try {
|
|
41
|
+
const csrfToken = readHeader(headers, csrfTokenHeaderName);
|
|
42
|
+
const cookie = readHeader(headers, 'cookie');
|
|
43
|
+
|
|
44
|
+
if (!cookie || !csrfToken) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const jwt = await extractCookieJwt(cookie, jwtParams);
|
|
49
|
+
|
|
50
|
+
if (!jwt || jwt.csrfToken !== csrfToken) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return jwt.userId;
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Used by host (backend) code to set headers on a response object.
|
|
62
|
+
*
|
|
63
|
+
* @category Auth : Host
|
|
64
|
+
*/
|
|
65
|
+
export async function generateSuccessfulLoginHeaders(
|
|
66
|
+
userId: string,
|
|
67
|
+
/** The id from your database of the user you're authenticating. */
|
|
68
|
+
cookieConfig: Readonly<CookieParams>,
|
|
69
|
+
) {
|
|
70
|
+
const csrfToken = generateCsrfToken();
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
'set-cookie': await generateCookie(
|
|
74
|
+
{
|
|
75
|
+
csrfToken,
|
|
76
|
+
userId,
|
|
77
|
+
},
|
|
78
|
+
cookieConfig,
|
|
79
|
+
),
|
|
80
|
+
[csrfTokenHeaderName]: csrfToken,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Store auth data on a client (frontend) after receiving an auth response from the host (backend).
|
|
86
|
+
* Specifically, this stores the CSRF token into local storage (which doesn't need to be a secret).
|
|
87
|
+
* Alternatively, if the given response failed, this will wipe the existing (if anyone) stored CSRF
|
|
88
|
+
* token.
|
|
89
|
+
*
|
|
90
|
+
* @category Auth : Client
|
|
91
|
+
* @throws Error if no CSRF token header is found.
|
|
92
|
+
*/
|
|
93
|
+
export function handleAuthResponse(
|
|
94
|
+
response: Readonly<Pick<Response, 'ok' | 'headers'>>,
|
|
95
|
+
overrides: {
|
|
96
|
+
/**
|
|
97
|
+
* Allows mocking or overriding the global `localStorage`.
|
|
98
|
+
*
|
|
99
|
+
* @default globalThis.localStorage
|
|
100
|
+
*/
|
|
101
|
+
localStorage?: Pick<Storage, 'setItem' | 'removeItem'>;
|
|
102
|
+
/** Override the default CSRF token header name. */
|
|
103
|
+
csrfHeaderName?: string;
|
|
104
|
+
} = {},
|
|
105
|
+
) {
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
wipeCurrentCsrfToken(overrides);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const headerName = overrides.csrfHeaderName || csrfTokenHeaderName;
|
|
111
|
+
|
|
112
|
+
const csrfToken = response.headers.get(headerName);
|
|
113
|
+
|
|
114
|
+
if (!csrfToken) {
|
|
115
|
+
wipeCurrentCsrfToken(overrides);
|
|
116
|
+
throw new Error('Did not receive any CSRF token.');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
(overrides.localStorage || globalThis.localStorage).setItem(headerName, csrfToken);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Used in client (frontend) code to retrieve the current CSRF token in order to send it with
|
|
124
|
+
* requests to the host (backend).
|
|
125
|
+
*
|
|
126
|
+
* @category Auth : Client
|
|
127
|
+
*/
|
|
128
|
+
export function getCurrentCsrfToken(
|
|
129
|
+
overrides: {
|
|
130
|
+
/**
|
|
131
|
+
* Allows mocking or overriding the global `localStorage`.
|
|
132
|
+
*
|
|
133
|
+
* @default globalThis.localStorage
|
|
134
|
+
*/
|
|
135
|
+
localStorage?: Pick<Storage, 'getItem'>;
|
|
136
|
+
/** Override the default CSRF token header name. */
|
|
137
|
+
csrfHeaderName?: string;
|
|
138
|
+
} = {},
|
|
139
|
+
): string | undefined {
|
|
140
|
+
return (
|
|
141
|
+
(overrides.localStorage || globalThis.localStorage).getItem(
|
|
142
|
+
overrides.csrfHeaderName || csrfTokenHeaderName,
|
|
143
|
+
) || undefined
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Wipes the current stored CSRF token. This should be used by client (frontend) code to logout a
|
|
149
|
+
* user or react to a session timeout.
|
|
150
|
+
*
|
|
151
|
+
* @category Auth : Client
|
|
152
|
+
*/
|
|
153
|
+
export function wipeCurrentCsrfToken(
|
|
154
|
+
overrides: {
|
|
155
|
+
/**
|
|
156
|
+
* Allows mocking or overriding the global `localStorage`.
|
|
157
|
+
*
|
|
158
|
+
* @default globalThis.localStorage
|
|
159
|
+
*/
|
|
160
|
+
localStorage?: Pick<Storage, 'removeItem'>;
|
|
161
|
+
/** Override the default CSRF token header name. */
|
|
162
|
+
csrfHeaderName?: string;
|
|
163
|
+
} = {},
|
|
164
|
+
) {
|
|
165
|
+
return (overrides.localStorage || globalThis.localStorage).removeItem(
|
|
166
|
+
overrides.csrfHeaderName || csrfTokenHeaderName,
|
|
167
|
+
);
|
|
168
|
+
}
|
package/src/cookie.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {check} from '@augment-vir/assert';
|
|
2
|
+
import {safeMatch, type PartialWithUndefined} from '@augment-vir/common';
|
|
3
|
+
import {convertDuration, type AnyDuration} from 'date-vir';
|
|
4
|
+
import {parseUrl} from 'url-vir';
|
|
5
|
+
import {CreateJwtParams, type ParseJwtParams} from './jwt.js';
|
|
6
|
+
import {createUserJwt, parseUserJwt, UserJwtData} from './user-jwt.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parameters for {@link generateCookie}.
|
|
10
|
+
*
|
|
11
|
+
* @category Internal
|
|
12
|
+
*/
|
|
13
|
+
export type CookieParams = {
|
|
14
|
+
/**
|
|
15
|
+
* The origin of the host (backend) service that cookies will be included in all requests to.
|
|
16
|
+
* This should be restricted to just your host (backend) origin for security purposes.
|
|
17
|
+
*
|
|
18
|
+
* @example 'https://www.example.com'
|
|
19
|
+
*/
|
|
20
|
+
hostOrigin: string;
|
|
21
|
+
/**
|
|
22
|
+
* The max duration of this cookie. Or, in other words, the max user session duration before
|
|
23
|
+
* they're logged out.
|
|
24
|
+
*/
|
|
25
|
+
cookieDuration: AnyDuration;
|
|
26
|
+
/**
|
|
27
|
+
* All JWT parameters required for generating the encrypted JWT that will be embedded in the
|
|
28
|
+
* Cookie. Note that all JWT keys contained herein should never shared with any frontend,
|
|
29
|
+
* client, etc.
|
|
30
|
+
*/
|
|
31
|
+
jwtParams: Readonly<CreateJwtParams>;
|
|
32
|
+
} & PartialWithUndefined<{
|
|
33
|
+
/**
|
|
34
|
+
* Is set to `true` (which should only be done in development environments), the cookie will be
|
|
35
|
+
* allowed in insecure requests (non HTTPS requests).
|
|
36
|
+
*
|
|
37
|
+
* @default false
|
|
38
|
+
*/
|
|
39
|
+
isDev: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generate a secure cookie that stores the user JWT data. Used in host (backend) code.
|
|
44
|
+
*
|
|
45
|
+
* @category Internal
|
|
46
|
+
*/
|
|
47
|
+
export async function generateCookie(
|
|
48
|
+
userJwtData: Readonly<UserJwtData>,
|
|
49
|
+
cookieConfig: Readonly<CookieParams>,
|
|
50
|
+
) {
|
|
51
|
+
return [
|
|
52
|
+
`auth=${await createUserJwt(userJwtData, cookieConfig.jwtParams)}`,
|
|
53
|
+
`Domain=${parseUrl(cookieConfig.hostOrigin).hostname}`,
|
|
54
|
+
'HttpOnly',
|
|
55
|
+
'Path=/',
|
|
56
|
+
'SameSite=Strict',
|
|
57
|
+
`MAX-AGE=${convertDuration(cookieConfig.cookieDuration, {seconds: true}).seconds}`,
|
|
58
|
+
cookieConfig.isDev ? '' : 'Secure',
|
|
59
|
+
]
|
|
60
|
+
.filter(check.isTruthy)
|
|
61
|
+
.join('; ');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract an auth cookie from a cookie string. Used in host (backend) code.
|
|
66
|
+
*
|
|
67
|
+
* @category Internal
|
|
68
|
+
* @returns The extracted auth Cookie JWT data or `undefined` if no valid auth JWT data was found.
|
|
69
|
+
*/
|
|
70
|
+
export async function extractCookieJwt(
|
|
71
|
+
rawCookie: string,
|
|
72
|
+
jwtParams: Readonly<ParseJwtParams>,
|
|
73
|
+
): Promise<undefined | UserJwtData> {
|
|
74
|
+
const [auth] = safeMatch(rawCookie, /auth=[^;]+(?:;|$)/);
|
|
75
|
+
|
|
76
|
+
if (!auth) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const rawJwt = auth.replace('auth=', '').replace(';', '');
|
|
81
|
+
|
|
82
|
+
const jwt = await parseUserJwt(rawJwt, jwtParams);
|
|
83
|
+
|
|
84
|
+
return jwt;
|
|
85
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {randomString} from '@augment-vir/common';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Name of the header attached to responses that stores the CSRF token value.
|
|
5
|
+
*
|
|
6
|
+
* @category Internal
|
|
7
|
+
*/
|
|
8
|
+
export const csrfTokenHeaderName = 'csrf-token';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generates a random, cryptographically secure CSRF token.
|
|
12
|
+
*
|
|
13
|
+
* @category Internal
|
|
14
|
+
*/
|
|
15
|
+
export function generateCsrfToken(): string {
|
|
16
|
+
return randomString(256);
|
|
17
|
+
}
|
package/src/hash.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {bcrypt, bcryptVerify} from 'hash-wasm';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hashes a password using the bcrypt algorithm so passwords don't need to be stored in plain text.
|
|
5
|
+
* The output of this function is safe to store in a database for future credential comparisons.
|
|
6
|
+
*
|
|
7
|
+
* @category Auth : Host
|
|
8
|
+
* @returns `undefined` if the password is too long (and would be truncated by the bcrypt hashing
|
|
9
|
+
* algorithm). Otherwise, the hashed output.
|
|
10
|
+
* @see https://wikipedia.org/wiki/Bcrypt
|
|
11
|
+
*/
|
|
12
|
+
export async function hashPassword(password: string): Promise<undefined | string> {
|
|
13
|
+
if (willHashTruncate(password)) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const salt = new Uint8Array(16);
|
|
18
|
+
globalThis.crypto.getRandomValues(salt);
|
|
19
|
+
|
|
20
|
+
return await bcrypt({
|
|
21
|
+
costFactor: 10,
|
|
22
|
+
password: password.normalize(),
|
|
23
|
+
salt,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Checks if the given string will be truncated when passed through {@link hashPassword}. Passwords
|
|
29
|
+
* longer than this should not be accepted.
|
|
30
|
+
*
|
|
31
|
+
* @category Internal
|
|
32
|
+
*/
|
|
33
|
+
export function willHashTruncate(input: string): boolean {
|
|
34
|
+
return getByteLength(input) > 72;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A utility that provides more accurate string byte size than doing `string.length`.
|
|
39
|
+
*
|
|
40
|
+
* @category Internal
|
|
41
|
+
*/
|
|
42
|
+
export function getByteLength(input: string): number {
|
|
43
|
+
return new Blob([input]).size;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Checks if the given password is a match by comparing it to its previously computed and stored
|
|
48
|
+
* hash.
|
|
49
|
+
*
|
|
50
|
+
* @category Auth : Host
|
|
51
|
+
*/
|
|
52
|
+
export async function doesPasswordMatchHash({
|
|
53
|
+
password,
|
|
54
|
+
hash,
|
|
55
|
+
}: {
|
|
56
|
+
/** The password entered by the user in their login attempt. */
|
|
57
|
+
password: string;
|
|
58
|
+
/** The stored password hash for that user. */
|
|
59
|
+
hash: string;
|
|
60
|
+
}): Promise<boolean> {
|
|
61
|
+
return await bcryptVerify({
|
|
62
|
+
hash,
|
|
63
|
+
password,
|
|
64
|
+
});
|
|
65
|
+
}
|
package/src/index.ts
ADDED
package/src/jwt-keys.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {assertWrap} from '@augment-vir/assert';
|
|
2
|
+
import {base64url} from 'jose';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The keys required to sign and encrypt the JWT in their raw form for storage in a secure secrets
|
|
6
|
+
* database (such as AWS Secrets Manager) for later parsing by {@link parseJwtKeys}.
|
|
7
|
+
*
|
|
8
|
+
* These keys should be kept secret and never shared with any frontend, client, etc.
|
|
9
|
+
*
|
|
10
|
+
* @category Internal
|
|
11
|
+
*/
|
|
12
|
+
export type RawJwtKeys = Readonly<{
|
|
13
|
+
encryptionKey: string;
|
|
14
|
+
signingKey: string;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The keys required to sign and encrypt the JWT.
|
|
19
|
+
*
|
|
20
|
+
* These keys should be kept secret and never shared with any frontend, client, etc.
|
|
21
|
+
*
|
|
22
|
+
* @category Internal
|
|
23
|
+
*/
|
|
24
|
+
export type JwtKeys = Readonly<{
|
|
25
|
+
/**
|
|
26
|
+
* Encryption key for JWTs. This is a Uint8Array because `EncryptJWT.encrypt` does not support
|
|
27
|
+
* `CryptoKey` for our chosen encryption algorithm.
|
|
28
|
+
*/
|
|
29
|
+
encryptionKey: Readonly<Uint8Array>;
|
|
30
|
+
/** Signing key for JWTs. */
|
|
31
|
+
signingKey: Readonly<CryptoKey>;
|
|
32
|
+
}>;
|
|
33
|
+
|
|
34
|
+
const signingKeyOptions: [HmacKeyGenParams, boolean, ReadonlyArray<KeyUsage>] = [
|
|
35
|
+
{
|
|
36
|
+
name: 'HMAC',
|
|
37
|
+
hash: 'SHA-512',
|
|
38
|
+
},
|
|
39
|
+
true,
|
|
40
|
+
[
|
|
41
|
+
'sign',
|
|
42
|
+
'verify',
|
|
43
|
+
],
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate fresh and serialized JWT signing and encryption keys. These should be stored in a secure
|
|
48
|
+
* secrets database (such as AWS Secrets Manager) for later parsing by {@link parseJwtKeys}.
|
|
49
|
+
*
|
|
50
|
+
* These keys should be kept secret and never shared with any frontend, client, etc.
|
|
51
|
+
*
|
|
52
|
+
* @category Keys
|
|
53
|
+
*/
|
|
54
|
+
export async function generateNewJwtKeys(): Promise<RawJwtKeys> {
|
|
55
|
+
return {
|
|
56
|
+
encryptionKey: assertWrap.isDefined(
|
|
57
|
+
(
|
|
58
|
+
await globalThis.crypto.subtle.exportKey(
|
|
59
|
+
'jwk',
|
|
60
|
+
await globalThis.crypto.subtle.generateKey(
|
|
61
|
+
{
|
|
62
|
+
name: 'AES-GCM',
|
|
63
|
+
length: 256,
|
|
64
|
+
},
|
|
65
|
+
true,
|
|
66
|
+
[
|
|
67
|
+
'encrypt',
|
|
68
|
+
'decrypt',
|
|
69
|
+
],
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
).k,
|
|
73
|
+
),
|
|
74
|
+
signingKey: assertWrap.isDefined(
|
|
75
|
+
(
|
|
76
|
+
await globalThis.crypto.subtle.exportKey(
|
|
77
|
+
'jwk',
|
|
78
|
+
await globalThis.crypto.subtle.generateKey(...signingKeyOptions),
|
|
79
|
+
)
|
|
80
|
+
).k,
|
|
81
|
+
),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parses an instance of {@link RawJwtKeys} and produces the final {@link JwtKeys} object required by
|
|
87
|
+
* all authentication functionality.
|
|
88
|
+
*
|
|
89
|
+
* @category Keys
|
|
90
|
+
*/
|
|
91
|
+
export async function parseJwtKeys(rawKeys: Readonly<RawJwtKeys>): Promise<Readonly<JwtKeys>> {
|
|
92
|
+
if (!rawKeys.encryptionKey) {
|
|
93
|
+
throw new Error('JWT encryption key is empty');
|
|
94
|
+
} else if (!rawKeys.signingKey) {
|
|
95
|
+
throw new Error('JWT signing key is empty');
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
encryptionKey: base64url.decode(rawKeys.encryptionKey),
|
|
99
|
+
signingKey: await crypto.subtle.importKey(
|
|
100
|
+
'jwk',
|
|
101
|
+
{
|
|
102
|
+
k: rawKeys.signingKey,
|
|
103
|
+
alg: 'HS512',
|
|
104
|
+
ext: signingKeyOptions[1],
|
|
105
|
+
key_ops: [...signingKeyOptions[2]],
|
|
106
|
+
kty: 'oct',
|
|
107
|
+
},
|
|
108
|
+
...signingKeyOptions,
|
|
109
|
+
),
|
|
110
|
+
};
|
|
111
|
+
}
|