diff --git a/docs/src/content/docs/api-reference/jose.mdx b/docs/src/content/docs/api-reference/jose.mdx index 7b985798..c6e175fe 100644 --- a/docs/src/content/docs/api-reference/jose.mdx +++ b/docs/src/content/docs/api-reference/jose.mdx @@ -30,9 +30,10 @@ Through this api reference documentation you are going to learn and understand f - [Features](#features) - [Installation](#installation) - [API Reference](#api-reference) - - [Signing and Encripting a JWT](#signing-and-encripting-a-jwt) - - [encodeJWT](#encode-jwt) - - [decodeJWT](#decode-jwt) + - [Signing and Encrypting a JWT](#signing-and-encrypting-a-jwt) + - [Signing API](#signing-api-jws) + - [Encryption API](#encryption-api-jwe) + - [Key Derivation](#key-derivation) --- @@ -43,6 +44,7 @@ Through this api reference documentation you are going to learn and understand f - **JWS utilities** — use `createJWS`, `signJWS`, and `verifyJWS` for signature and verification. - **JWT helpers** — use `createJWT`, `encodeJWT`, and `decodeJWT` for signing and encrypting JSON Web Tokens. - **Secure by design** — built on top of modern JOSE standards (RFC 7515, 7516, 7519). +- **Key derivation** — use `deriveKey` for key derivation --- @@ -63,13 +65,13 @@ npm install @aura-stack/jose ## API Reference -## Signing and Encripting a JWT +## Signing and Encrypting a JWT ### `encodeJWT(payload, secret)` Encodes a JWT by first signing it (JWS) and then encrypting it (JWE). This ensures both **integrity** and **confidentiality** as recommended by [RFC 7519 #11.2](https://datatracker.ietf.org/doc/html/rfc7519#section-11.2). -```ts +```ts lineNumbers type encodeJWT = (token: JWTPayload, secret: SecretInput) => Promise ``` @@ -86,7 +88,7 @@ Promise resolving to the signed and encrypted JWT string. #### Example -```typescript +```ts lineNumbers import { encodeJWT } from "@aura-stack/jose" const token = await encodeJWT( @@ -111,7 +113,7 @@ Decodes a JWT by first decrypting it (JWE) and then verifying it (JWS). This val #### Signature -```typescript +```ts lineNumbers function decodeJWT(token: string, secret: SecretInput): Promise ``` @@ -128,7 +130,7 @@ Promise resolving to the decoded JWT payload. #### Example -```typescript +```ts lineNumbers import { decodeJWT } from "@aura-stack/jose" try { @@ -150,7 +152,7 @@ Creates a JWT handler with bound `encodeJWT` and `decodeJWT` methods. #### Signature -```typescript +```ts lineNumbers function createJWT(secret: SecretInput): { encodeJWT: (payload: JWTPayload) => Promise decodeJWT: (token: string) => Promise @@ -169,7 +171,7 @@ Object with `encodeJWT` and `decodeJWT` methods. #### Example -```typescript +```ts lineNumbers import { createJWT } from "@aura-stack/jose" const jwt = createJWT(process.env.JWT_SECRET!) @@ -189,7 +191,7 @@ Signs a JWT using HS256 algorithm with standard claims. #### Signature -```typescript +```ts lineNumbers function signJWS(payload: JWTPayload, secret: SecretInput): Promise ``` @@ -208,7 +210,7 @@ The following claims are automatically added: #### Example -```typescript +```ts lineNumbers import { signJWS } from "@aura-stack/jose" const signed = await signJWS( @@ -230,7 +232,7 @@ Verifies a signed JWT and returns the payload if valid. #### Signature -```typescript +```ts lineNumbers function verifyJWS(token: string, secret: SecretInput): Promise ``` @@ -242,7 +244,7 @@ function verifyJWS(token: string, secret: SecretInput): Promise #### Example -```typescript +```ts lineNumbers import { verifyJWS } from "@aura-stack/jose" try { @@ -261,7 +263,7 @@ Creates a JWS handler with bound signing and verification methods. #### Signature -```typescript +```ts lineNumbers function createJWS(secret: SecretInput): { signJWS: (payload: JWTPayload) => Promise verifyJWS: (token: string) => Promise @@ -270,7 +272,7 @@ function createJWS(secret: SecretInput): { #### Example -```typescript +```ts lineNumbers import { createJWS } from "@aura-stack/jose" const jws = createJWS(process.env.JWT_SECRET!) @@ -287,7 +289,7 @@ Encrypts a JWT string using A256GCM encryption. #### Signature -```typescript +```ts lineNumbers function encryptJWE(payload: string, secret: SecretInput): Promise ``` @@ -306,7 +308,7 @@ function encryptJWE(payload: string, secret: SecretInput): Promise #### Example -```typescript +```ts lineNumbers import { encryptJWE } from "@aura-stack/jose" const encrypted = await encryptJWE("signed-jwt-token-here", process.env.ENCRYPTION_KEY!) @@ -320,13 +322,13 @@ Decrypts an encrypted JWT and returns the original payload string. #### Signature -```typescript +```ts lineNumbers function decryptJWE(token: string, secret: SecretInput): Promise ``` #### Example -```typescript +```ts lineNumbers import { decryptJWE } from "@aura-stack/jose" try { @@ -345,7 +347,7 @@ Creates a JWE handler with bound encryption and decryption methods. #### Signature -```typescript +```ts lineNumbers function createJWE(secret: SecretInput): { encryptJWE: (payload: string) => Promise decryptJWE: (token: string) => Promise @@ -354,7 +356,7 @@ function createJWE(secret: SecretInput): { #### Example -```typescript +```ts lineNumbers import { createJWE } from "@aura-stack/jose" const jwe = createJWE(process.env.ENCRYPTION_KEY!) @@ -363,19 +365,82 @@ const encrypted = await jwe.encryptJWE("data-to-encrypt") const decrypted = await jwe.decryptJWE(encrypted) ``` +## Key Derivation + +### `deriveKey(secret, info, len)` + +Create a Key derivation which implements HKDF + +#### Signature + +```ts lineNumbers +import type { SecretInput } from "@aura-stack/jose" + +function deriveKey( + secret: SecretInput, + info: string, + len: number +): { + key: ArrayBuffer + deriveKey: Buffer +} +``` + +#### Example + +```ts lineNumbers +import { deriveKey } from "@aura-stack/jose/hkdf" + +const secret = "secret-key" + +const sessionKey = deriveKey(secret, "session key", 32) +const databaseKey = deriveKey(secret, "database key", 64) +``` + +### `createDeriveKey(secret, info, len)` + +Creates a key derivation function that includes verification for the key length. It implements the `deriveKey` function. + +#### Signature + +```ts lineNumbers +import type { SecretInput } from "@aura-stack/jose" + +function createDeriveKey( + secret: SecretInput, + info: string, + len: number +): { + key: ArrayBuffer + deriveKey: Buffer +} +``` + +#### Example + +```ts lineNumbers +import { createDeriveKey } from "@aura-stack/jose/hkdf" + +const secret = "secret-key" + +const sessionKey = createDeriveKey(secret, "session key", 32) +const databaseKey = createDeriveKey(secret, "database key", 64) +``` + +--- + ## Types ### `SecretInput` Flexible secret key input type. -```typescript -type SecretInput = CryptoKey | KeyObject | string | Uint8Array +```ts lineNumbers +type SecretInput = KeyObject | Uint8Array | string ``` Accepts: -- `CryptoKey` - Web Crypto API key - `KeyObject` - Node.js crypto key - `string` - String secret (converted internally) - `Uint8Array` - Binary secret @@ -384,7 +449,7 @@ Accepts: Standard JWT payload with registered claims. -```typescript +```ts lineNumbers interface JWTPayload { iss?: string // Issuer sub?: string // Subject (user ID) @@ -409,7 +474,7 @@ interface JWTPayload { Always use cryptographically secure random strings: -```typescript +```ts lineNumbers // ✅ Good: Strong random secret import crypto from "node:crypto" const secret = crypto.randomBytes(32).toString("hex") @@ -426,7 +491,7 @@ const secret = "password123" Never hardcode secrets: -```typescript +```ts lineNumbers // ✅ Good: Environment variable const secret = process.env.JWT_SECRET @@ -463,11 +528,13 @@ Always transmit JWTs over HTTPS in production to prevent token interception. +--- + ## Usage ### With Session Management -```typescript +```ts lineNumbers import { encodeJWT, decodeJWT } from "@aura-stack/jose" // Create session token @@ -510,7 +577,7 @@ if (userId) { This package is built on top of [`jose`](https://github.com/panva/jose) - a robust implementation of JOSE standards. For advanced use cases, you can access the underlying library directly: -```typescript +```ts lineNumbers import * as jose from "jose" // Full access to jose library diff --git a/packages/core/src/jose.ts b/packages/core/src/jose.ts index 080972aa..9bfc55a9 100644 --- a/packages/core/src/jose.ts +++ b/packages/core/src/jose.ts @@ -4,43 +4,8 @@ export type { JWTPayload } from "@aura-stack/jose/jose" const secretKey = process.env.AURA_AUTH_SECRET! -let jwtInstance: ReturnType | null = null -let jwsInstance: ReturnType | null = null +const { derivedKey: derivedSessionKey } = createDeriveKey(secretKey, "session") +const { derivedKey: derivedCsrfTokenKey } = createDeriveKey(secretKey, "csrfToken") -/** - * in ES2020 modules, is not allowed to have top-level await, so we create the instances lazily - * and cache them for future use. as well, cmjs modules do not support top-level await either. - */ -const createJoseInstance = async () => { - if (jwtInstance && jwsInstance) { - return { jwtInstance, jwsInstance } - } - - const { derivedKey: derivedSessionKey } = await createDeriveKey(secretKey, "session") - const { derivedKey: derivedCsrfTokenKey } = await createDeriveKey(secretKey, "csrfToken") - - jwtInstance = createJWT(derivedSessionKey) - jwsInstance = createJWS(derivedCsrfTokenKey) - - return { jwtInstance, jwsInstance } -} - -export const encodeJWT = async (...args: Parameters["encodeJWT"]>) => { - const { jwtInstance } = await createJoseInstance() - return jwtInstance.encodeJWT(...args) -} - -export const decodeJWT = async (...args: Parameters["decodeJWT"]>) => { - const { jwtInstance } = await createJoseInstance() - return jwtInstance.decodeJWT(...args) -} - -export const signJWS = async (...args: Parameters["signJWS"]>) => { - const { jwsInstance } = await createJoseInstance() - return jwsInstance.signJWS(...args) -} - -export const verifyJWS = async (...args: Parameters["verifyJWS"]>) => { - const { jwsInstance } = await createJoseInstance() - return jwsInstance.verifyJWS(...args) -} +export const { decodeJWT, encodeJWT } = createJWT(derivedSessionKey) +export const { signJWS, verifyJWS } = createJWS(derivedCsrfTokenKey) diff --git a/packages/jose/src/deriveKey.ts b/packages/jose/src/deriveKey.ts index ea528fe7..968dbbac 100644 --- a/packages/jose/src/deriveKey.ts +++ b/packages/jose/src/deriveKey.ts @@ -1,4 +1,6 @@ -import { hkdfSync, randomBytes, type BinaryLike, type KeyObject } from "node:crypto" +import { hkdfSync, randomBytes } from "node:crypto" +import { createSecret } from "@/secret.js" +import type { SecretInput } from "@/index.js" /** * Generate a derived key using HKDF (HMAC-based Extract-and-Expand Key Derivation Function) @@ -8,7 +10,7 @@ import { hkdfSync, randomBytes, type BinaryLike, type KeyObject } from "node:cry * @param length Size of the derived key in bytes (default is 32 bytes) * @returns Derived key as Uint8Array and base64 encoded string */ -export const deriveKey = async (secret: BinaryLike | KeyObject, info: string, length: number = 32) => { +export const deriveKey = (secret: SecretInput, info: string, length: number = 32) => { try { const salt = randomBytes(length) const key = hkdfSync("SHA256", secret, salt, info, length) @@ -29,7 +31,7 @@ export const deriveKey = async (secret: BinaryLike | KeyObject, info: string, le * @param secret - The secret as a string or Uint8Array * @returns The secret in Uint8Array format */ -export const createDeriveKey = async (secret: BinaryLike | KeyObject, info?: string, length: number = 32) => { - if (secret === undefined) throw new Error("Secret is required") - return await deriveKey(secret, info ?? "Aura Jose secret derivation", length) +export const createDeriveKey = (secret: SecretInput, info?: string, length: number = 32) => { + const secretKey = createSecret(secret) + return deriveKey(secretKey, info ?? "Aura Jose secret derivation", length) } diff --git a/packages/jose/src/index.ts b/packages/jose/src/index.ts index 09ab2b5e..31e03870 100644 --- a/packages/jose/src/index.ts +++ b/packages/jose/src/index.ts @@ -10,7 +10,7 @@ export * from "@/sign.js" export * from "@/encrypt.js" export * from "@/deriveKey.js" -export type SecretInput = CryptoKey | KeyObject | string | Uint8Array +export type SecretInput = KeyObject | Uint8Array | string /** * Encode a JWT signed and encrypted token. The token first signed using JWS diff --git a/packages/jose/test/index.test.ts b/packages/jose/test/index.test.ts index 88ab2e81..36bf4d26 100644 --- a/packages/jose/test/index.test.ts +++ b/packages/jose/test/index.test.ts @@ -1,11 +1,11 @@ import crypto from "node:crypto" import { describe, test, expect } from "vitest" import type { JWTPayload } from "jose" -import { createJWS, signJWS, verifyJWS } from "@/sign.js" -import { createJWE, encryptJWE, decryptJWE } from "@/encrypt.js" import { createJWT } from "@/index.js" -import { deriveKey, createDeriveKey } from "@/deriveKey.js" import { createSecret } from "@/secret.js" +import { createJWS, signJWS, verifyJWS } from "@/sign.js" +import { deriveKey, createDeriveKey } from "@/deriveKey.js" +import { createJWE, encryptJWE, decryptJWE } from "@/encrypt.js" const payload: JWTPayload = { sub: "user-123", @@ -16,7 +16,7 @@ const payload: JWTPayload = { describe("JWSs", () => { test("sign and verify a JWS using signJWS and verifyJWS", async () => { const secretKey = crypto.randomBytes(32) - const { derivedKey } = await createDeriveKey(secretKey) + const { derivedKey } = createDeriveKey(secretKey) const jws = await signJWS(payload, derivedKey) expect(jws).toBeDefined() @@ -29,7 +29,7 @@ describe("JWSs", () => { test("sign and verify a JWS using createJWS", async () => { const secretKey = crypto.randomBytes(32) - const { derivedKey } = await createDeriveKey(secretKey) + const { derivedKey } = createDeriveKey(secretKey) const { signJWS, verifyJWS } = createJWS(derivedKey) @@ -49,7 +49,7 @@ describe("JWSs", () => { test("fail JWT to try to verify a JWS with invalid secret", async () => { const secretKey = crypto.randomBytes(32) - const { derivedKey } = await createDeriveKey(secretKey) + const { derivedKey } = createDeriveKey(secretKey) const jws = await signJWS(payload, derivedKey) expect(jws).toBeDefined() @@ -62,7 +62,7 @@ describe("JWSs", () => { describe("JWEs", () => { test("encrypt and decrypt a JWE using encryptJWE and decryptJWE", async () => { const secretKey = crypto.randomBytes(32) - const { derivedKey } = await createDeriveKey(secretKey) + const { derivedKey } = createDeriveKey(secretKey) const jwe = await encryptJWE(JSON.stringify(payload), derivedKey) expect(jwe).toBeDefined() @@ -77,7 +77,7 @@ describe("JWEs", () => { test("encrypt and decrypt a JWE using createJWE", async () => { const secretKey = crypto.randomBytes(32) - const { derivedKey } = await createDeriveKey(secretKey) + const { derivedKey } = createDeriveKey(secretKey) const { signJWS } = createJWS(derivedKey) const { encryptJWE, decryptJWE } = createJWE(derivedKey) @@ -92,7 +92,7 @@ describe("JWEs", () => { test("fail JWT to try to decrypt an invalid JWE", async () => { const secretKey = crypto.randomBytes(32) - const { derivedKey } = await createDeriveKey(secretKey) + const { derivedKey } = createDeriveKey(secretKey) const { decryptJWE } = createJWE(derivedKey) await expect(decryptJWE("header.payload.signature")).rejects.toThrow() @@ -102,7 +102,7 @@ describe("JWEs", () => { describe("JWTs", () => { test("create a signed and encrypted JWT using createJWS and createJWE functions", async () => { const secretKey = crypto.randomBytes(32) - const { derivedKey } = await createDeriveKey(secretKey) + const { derivedKey } = createDeriveKey(secretKey) const { signJWS, verifyJWS } = createJWS(derivedKey) const { encryptJWE, decryptJWE } = createJWE(derivedKey) @@ -122,7 +122,7 @@ describe("JWTs", () => { test("create a signed and encrypted JWT using createJWT function", async () => { const secret = crypto.randomBytes(32) - const { encodeJWT, decodeJWT } = await createJWT(secret) + const { encodeJWT, decodeJWT } = createJWT(secret) const jwt = await encodeJWT(payload) expect(jwt).toBeDefined() @@ -135,7 +135,7 @@ describe("JWTs", () => { test("fail JWT to try to decode an invalid JWT", async () => { const secret = crypto.randomBytes(32) - const { decodeJWT } = await createJWT(secret) + const { decodeJWT } = createJWT(secret) await expect(decodeJWT("invalid.jwt.token")).rejects.toThrow() }) @@ -143,7 +143,7 @@ describe("JWTs", () => { /** * Jose expects a secret of at least 32 bytes for HS256 */ - const { encodeJWT } = await createJWT("short") + const { encodeJWT } = createJWT("short") await expect(encodeJWT(payload)).rejects.toThrow() }) }) @@ -174,25 +174,23 @@ describe("createSecret", () => { }) describe("createDeriveKey", () => { - test("createDeriveKey", async () => { - const { key, derivedKey } = await createDeriveKey("adfasdf") - expect(derivedKey).toBeDefined() - expect(key.byteLength).toBe(32) + test("createDeriveKey", () => { + expect(() => createDeriveKey("adfasdf")).toThrow(/Secret string must be at least 32 characters long/) }) - test("createDeriveKey with 32 bytes", async () => { + test("createDeriveKey with 32 bytes", () => { const secretKey = crypto.randomBytes(32) - const { key, derivedKey } = await createDeriveKey(secretKey) + const { key, derivedKey } = createDeriveKey(secretKey) expect(derivedKey).toBeDefined() expect(key.byteLength).toBe(32) }) }) describe("deriveKey", () => { - test("deriveKey", async () => { + test("deriveKey", () => { const secret = "my-secret-password-123" - const derivedKey1 = await deriveKey(secret, "derive-1") - const derivedKey2 = await deriveKey(secret, "derive-2") + const derivedKey1 = deriveKey(secret, "derive-1") + const derivedKey2 = deriveKey(secret, "derive-2") expect(derivedKey1).toBeDefined() expect(derivedKey2).toBeDefined() expect(derivedKey1).not.toBe(derivedKey2)