Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased]

### Added

- Re-export the `encryptJWE` and `decryptJWE` functions for JWEs (Json Web Encryption) from the `jose` instance created from `createAuth` function. These functions are used internally for session and csrf token management and can be consumed for external reasons designed by the users. [#45](https://github.com/aura-stack-ts/auth/pull/45)

### Changed

- Updated `cookies` configuration option in `createAuth` function to support granular per-cookie settings for all internal cookies used by Aura Auth (e.g., `state`, `redirect_to`, `code_verifier`, `sessionToken`, and `csrfToken`) using the overrides object. Renamed `name` to `prefix` field to add to all of the cookies (without cookie prefixes). [#43](https://github.com/aura-stack-ts/auth/pull/43)
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/@types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ export interface JoseInstance {
encodeJWT: (payload: JWTPayload) => Promise<string>
signJWS: (payload: JWTPayload) => Promise<string>
verifyJWS: (payload: string) => Promise<JWTPayload>
encryptJWE: (payload: string) => Promise<string>
decryptJWE: (payload: string) => Promise<string>
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/actions/callback/access-token.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { formatZodError } from "@/utils.js"
import { AuthInternalError, OAuthProtocolError } from "@/errors.js"
import { OAuthAccessToken, OAuthAccessTokenErrorResponse, OAuthAccessTokenResponse } from "@/schemas.js"
import type { OAuthProviderCredentials } from "@/@types/index.js"
import { formatZodError } from "@/utils.js"

/**
* Make a request to the OAuth provider to the token endpoint to exchange the authorization code provided
Expand All @@ -22,7 +22,7 @@ export const createAccessToken = async (
) => {
const parsed = OAuthAccessToken.safeParse({ ...oauthConfig, redirectURI, code, codeVerifier })
if (!parsed.success) {
const msg = formatZodError(parsed.error).toString()
const msg = JSON.stringify(formatZodError(parsed.error), null, 2)
throw new AuthInternalError("INVALID_OAUTH_CONFIGURATION", msg)
}
const { accessToken, clientId, clientSecret, code: codeParsed, redirectURI: redirectParsed } = parsed.data
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/actions/callback/userinfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ export const getUserInfo = async (oauthConfig: OAuthProviderCredentials, accessT
}
return oauthConfig?.profile ? oauthConfig.profile(json) : getDefaultUserInfo(json)
} catch (error) {
if(isOAuthProtocolError(error)) {
if (isOAuthProtocolError(error)) {
throw error
}
if(isNativeError(error)) {
if (isNativeError(error)) {
throw new OAuthProtocolError("invalid_request", error.message, "", { cause: error })
}
throw new OAuthProtocolError("invalid_request", "Failed to fetch user information.", "", { cause: error })
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/actions/signIn/authorization.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isValidURL } from "@/assert.js"
import { OAuthAuthorization } from "@/schemas.js"
import { equals, formatZodError, getNormalizedOriginPath, sanitizeURL, toCastCase } from "@/utils.js"
import { AuthInternalError, AuthSecurityError, isAuthSecurityError } from "@/errors.js"
import { equals, formatZodError, getNormalizedOriginPath, sanitizeURL, toCastCase } from "@/utils.js"
import type { OAuthProviderCredentials } from "@/@types/index.js"

/**
Expand All @@ -26,7 +26,6 @@ export const createAuthorizationURL = (
const parsed = OAuthAuthorization.safeParse({ ...oauthConfig, redirectURI, state, codeChallenge, codeChallengeMethod })
if (!parsed.success) {
const msg = JSON.stringify(formatZodError(parsed.error), null, 2)
console.log("OAuth Authorization URL Creation Error:", msg)
throw new AuthInternalError("INVALID_OAUTH_CONFIGURATION", msg)
}
const { authorizeURL, ...options } = parsed.data
Expand Down
19 changes: 13 additions & 6 deletions packages/core/src/jose.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import "dotenv/config"
import { createJWT, createJWS, createDeriveKey } from "@aura-stack/jose"
import { createDerivedSalt } from "./secure.js"
import { AuthInternalError } from "./errors.js"
import { createJWT, createJWS, createJWE, createDeriveKey } from "@aura-stack/jose"
import { createDerivedSalt } from "@/secure.js"
import { AuthInternalError } from "@/errors.js"
export type { JWTPayload } from "@aura-stack/jose/jose"

/**
Expand All @@ -16,20 +16,27 @@ export type { JWTPayload } from "@aura-stack/jose/jose"
export const createJoseInstance = (secret?: string) => {
secret ??= process.env.AURA_AUTH_SECRET!
if (!secret) {
throw new AuthInternalError("JOSE_INITIALIZATION_FAILED", "AURA_AUTH_SECRET environment variable is not set and no secret was provided.")
throw new AuthInternalError(
"JOSE_INITIALIZATION_FAILED",
"AURA_AUTH_SECRET environment variable is not set and no secret was provided."
)
}

const salt = process.env.AURA_AUTH_SALT ?? createDerivedSalt(secret)
const { derivedKey: derivedSessionKey } = createDeriveKey(secret, salt, "session")
const { derivedKey: derivedSigningKey } = createDeriveKey(secret, salt, "signing")
const { derivedKey: derivedEncryptionKey } = createDeriveKey(secret, salt, "encryption")
const { derivedKey: derivedCsrfTokenKey } = createDeriveKey(secret, salt, "csrfToken")

const { decodeJWT, encodeJWT } = createJWT(derivedSessionKey)
const { decodeJWT, encodeJWT } = createJWT({ jws: derivedSigningKey, jwe: derivedEncryptionKey })
const { signJWS, verifyJWS } = createJWS(derivedCsrfTokenKey)
const { encryptJWE, decryptJWE } = createJWE(derivedEncryptionKey)

return {
decodeJWT,
encodeJWT,
signJWS,
verifyJWS,
encryptJWE,
decryptJWE,
}
}
5 changes: 1 addition & 4 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,7 @@ export const isValidRelativePath = (path: string | undefined | null): boolean =>
export const onErrorHandler: RouterConfig["onError"] = (error) => {
if (isRouterError(error)) {
const { message, status, statusText } = error
return Response.json(
{ type: "ROUTER_ERROR", code: "ROUTER_INTERNAL_ERROR", message },
{ status, statusText }
)
return Response.json({ type: "ROUTER_ERROR", code: "ROUTER_INTERNAL_ERROR", message }, { status, statusText })
}
if (isInvalidZodSchemaError(error)) {
return Response.json({ type: "ROUTER_ERROR", code: "INVALID_REQUEST", message: error.errors }, { status: 422 })
Expand Down
4 changes: 4 additions & 0 deletions packages/jose/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased]

### Added

- Added Key derivation support to `createJWT`, `encodeJWT` and `decodeJWT` functions which allows to pass the separated keys for signing and encrypting the JWTs using the `jws` and `jwe` properties as argument in the functions. [#45](https://github.com/aura-stack-ts/auth/pull/45)

---

## [0.1.0] - 2025-12-28
Expand Down
7 changes: 5 additions & 2 deletions packages/jose/src/assert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ export const isFalsy = (value: unknown): boolean => {
return value === null || value === undefined || value === false || value === 0 || value === "" || Number.isNaN(value)
}

export const isObject = (value: unknown): value is Record<string, unknown> => {
return typeof value === "object" && value !== null && !Array.isArray(value)
}

export const isInvalidPayload = (payload: unknown): boolean => {
return (
isFalsy(payload) ||
typeof payload !== "object" ||
Array.isArray(payload) ||
!isObject(payload) ||
(typeof payload === "object" && payload !== null && !Array.isArray(payload) && Object.keys(payload).length === 0)
)
}
4 changes: 2 additions & 2 deletions packages/jose/src/encrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface EncryptedPayload {
*/
export const encryptJWE = async (payload: string, secret: SecretInput) => {
try {
if(isFalsy(payload)) {
if (isFalsy(payload)) {
throw new InvalidPayloadError("The payload must be a non-empty string")
}
const secretKey = createSecret(secret)
Expand Down Expand Up @@ -52,7 +52,7 @@ export const encryptJWE = async (payload: string, secret: SecretInput) => {
*/
export const decryptJWE = async (token: string, secret: SecretInput) => {
try {
if(isFalsy(token)) {
if (isFalsy(token)) {
throw new InvalidPayloadError("The token must be a non-empty string")
}
const secretKey = createSecret(secret)
Expand Down
18 changes: 11 additions & 7 deletions packages/jose/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import { createJWE } from "@/encrypt.js"
import { isAuraJoseError } from "@/assert.js"
import { JWTDecodingError, JWTEncodingError } from "./errors.js"
import type { KeyObject } from "node:crypto"
import { getSecrets } from "./secret.js"

export * from "@/sign.js"
export * from "@/encrypt.js"
export * from "@/deriveKey.js"

export type SecretInput = KeyObject | Uint8Array | string
export type DerivedKeyInput = { jws: SecretInput; jwe: SecretInput }

/**
* Encode a JWT signed and encrypted token. The token first signed using JWS
Expand All @@ -28,10 +30,11 @@ export type SecretInput = KeyObject | Uint8Array | string
* @param secret - Secret key used for both signing and encrypting the JWT
* @returns Promise resolving to the signed and encrypted JWT string
*/
export const encodeJWT = async (token: JWTPayload, secret: SecretInput) => {
export const encodeJWT = async (token: JWTPayload, secret: SecretInput | DerivedKeyInput) => {
try {
const { signJWS } = createJWS(secret)
const { encryptJWE } = createJWE(secret)
const { jweSecret, jwsSecret } = getSecrets(secret)
const { signJWS } = createJWS(jwsSecret)
const { encryptJWE } = createJWE(jweSecret)
const signed = await signJWS(token)
return await encryptJWE(signed)
} catch (error) {
Expand All @@ -54,10 +57,11 @@ export const encodeJWT = async (token: JWTPayload, secret: SecretInput) => {
* @param secret
* @returns
*/
export const decodeJWT = async (token: string, secret: SecretInput) => {
export const decodeJWT = async (token: string, secret: SecretInput | DerivedKeyInput) => {
try {
const { verifyJWS } = createJWS(secret)
const { decryptJWE } = createJWE(secret)
const { jweSecret, jwsSecret } = getSecrets(secret)
const { verifyJWS } = createJWS(jwsSecret)
const { decryptJWE } = createJWE(jweSecret)
const decrypted = await decryptJWE(token)
return await verifyJWS(decrypted)
} catch (error) {
Expand All @@ -76,7 +80,7 @@ export const decodeJWT = async (token: string, secret: SecretInput) => {
* @param secret - Secret key used for signing, verifying, encrypting and decrypting the JWT
* @returns JWT handler object with `signJWS/encryptJWE` and `verifyJWS/decryptJWE` methods
*/
export const createJWT = (secret: SecretInput) => {
export const createJWT = (secret: SecretInput | DerivedKeyInput) => {
return {
encodeJWT: async (payload: JWTPayload) => encodeJWT(payload, secret),
decodeJWT: async (token: string) => decodeJWT(token, secret),
Expand Down
14 changes: 12 additions & 2 deletions packages/jose/src/secret.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { InvalidSecretError } from "@/errors.js"
import type { SecretInput } from "@/index.js"
import { isObject } from "@/assert.js"
import type { DerivedKeyInput, SecretInput } from "@/index.js"

/**
* Create a secret in Uint8Array format
Expand All @@ -8,7 +9,7 @@ import type { SecretInput } from "@/index.js"
* @returns The secret in Uint8Array format
*/
export const createSecret = (secret: SecretInput) => {
if (secret === undefined) throw new Error("Secret is required")
if (secret === undefined) throw new InvalidSecretError("Secret is required")
if (typeof secret === "string") {
if (new TextEncoder().encode(secret).byteLength < 32) {
throw new InvalidSecretError("Secret string must be at least 32 characters long")
Expand All @@ -17,3 +18,12 @@ export const createSecret = (secret: SecretInput) => {
}
return secret
}

export const getSecrets = (secret: SecretInput | DerivedKeyInput) => {
const jwsSecret = isObject(secret) && "jws" in secret ? secret.jws : secret
const jweSecret = isObject(secret) && "jwe" in secret ? secret.jwe : secret
return {
jwsSecret,
jweSecret,
}
}
4 changes: 2 additions & 2 deletions packages/jose/src/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ export const signJWS = async (payload: JWTPayload, secret: SecretInput): Promise
*/
export const verifyJWS = async (token: string, secret: SecretInput): Promise<JWTPayload> => {
try {
if(isFalsy(token)) {
if (isFalsy(token)) {
throw new InvalidPayloadError("The token must be a non-empty string")
}
}
const secretKey = createSecret(secret)
const { payload } = await jwtVerify(token, secretKey)
return payload
Expand Down
15 changes: 15 additions & 0 deletions packages/jose/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,21 @@ describe("JWTs", () => {
const { encodeJWT } = createJWT("short")
await expect(encodeJWT(payload)).rejects.toThrow("Secret string must be at least 32 characters long")
})

test("create a signed and encrypted JWT using createJWT with separate JWS and JWE secrets", async () => {
const secret = crypto.randomBytes(32)
const { derivedKey: derivedSigningKey } = createDeriveKey(secret, "salt", "signing")
const { derivedKey: derivedEncryptionKey } = createDeriveKey(secret, "salt", "encryption")

const { encodeJWT, decodeJWT } = createJWT({ jws: derivedSigningKey, jwe: derivedEncryptionKey })

const jwt = await encodeJWT(payload)
expect(jwt).toBeDefined()
const decodedPayload = await decodeJWT(jwt)
expect(decodedPayload.sub).toBe(payload.sub)
expect(decodedPayload.name).toBe(payload.name)
expect(decodedPayload.email).toBe(payload.email)
})
})

describe("createSecret", () => {
Expand Down