auth-vir
Version:
Auth made easy and secure via JWT cookies, CSRF tokens, and password hashing helpers.
112 lines (106 loc) • 3.29 kB
text/typescript
import {assertWrap} from '@augment-vir/assert';
import {base64url} from 'jose';
/**
* The keys required to sign and encrypt the JWT in their raw form for storage in a secure secrets
* database (such as AWS Secrets Manager) for later parsing by {@link parseJwtKeys}.
*
* These keys should be kept secret and never shared with any frontend, client, etc.
*
* @category Internal
*/
export type RawJwtKeys = Readonly<{
encryptionKey: string;
signingKey: string;
}>;
/**
* The keys required to sign and encrypt the JWT.
*
* These keys should be kept secret and never shared with any frontend, client, etc.
*
* @category Internal
*/
export type JwtKeys = Readonly<{
/**
* Encryption key for JWTs. This is a Uint8Array because `EncryptJWT.encrypt` does not support
* `CryptoKey` for our chosen encryption algorithm.
*/
encryptionKey: Readonly<Uint8Array>;
/** Signing key for JWTs. */
signingKey: Readonly<CryptoKey>;
}>;
const signingKeyOptions: [HmacKeyGenParams, boolean, ReadonlyArray<KeyUsage>] = [
{
name: 'HMAC',
hash: 'SHA-512',
},
true,
[
'sign',
'verify',
],
];
/**
* Generate fresh and serialized JWT signing and encryption keys. These should be stored in a secure
* secrets database (such as AWS Secrets Manager) for later parsing by {@link parseJwtKeys}.
*
* These keys should be kept secret and never shared with any frontend, client, etc.
*
* @category Keys
*/
export async function generateNewJwtKeys(): Promise<RawJwtKeys> {
return {
encryptionKey: assertWrap.isDefined(
(
await globalThis.crypto.subtle.exportKey(
'jwk',
await globalThis.crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 256,
},
true,
[
'encrypt',
'decrypt',
],
),
)
).k,
),
signingKey: assertWrap.isDefined(
(
await globalThis.crypto.subtle.exportKey(
'jwk',
await globalThis.crypto.subtle.generateKey(...signingKeyOptions),
)
).k,
),
};
}
/**
* Parses an instance of {@link RawJwtKeys} and produces the final {@link JwtKeys} object required by
* all authentication functionality.
*
* @category Keys
*/
export async function parseJwtKeys(rawKeys: Readonly<RawJwtKeys>): Promise<Readonly<JwtKeys>> {
if (!rawKeys.encryptionKey) {
throw new Error('JWT encryption key is empty');
} else if (!rawKeys.signingKey) {
throw new Error('JWT signing key is empty');
}
return {
encryptionKey: base64url.decode(rawKeys.encryptionKey),
signingKey: await crypto.subtle.importKey(
'jwk',
{
k: rawKeys.signingKey,
alg: 'HS512',
ext: signingKeyOptions[1],
key_ops: [...signingKeyOptions[2]],
kty: 'oct',
},
...signingKeyOptions,
),
};
}