Skip to content

Commit f4b6011

Browse files
authored
feat(jose): add key derivation support to createJWT (#45)
* feat(jose): add key derivation support to createJWT * chore(jose): update createJWT implementation * feat(core): re-export `encryptJWE` and `decryptJWE` * docs: update `CHANGELOG.md` files
1 parent dcc66cd commit f4b6011

File tree

14 files changed

+76
-31
lines changed

14 files changed

+76
-31
lines changed

packages/core/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
88

99
## [Unreleased]
1010

11+
### Added
12+
13+
- 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)
14+
1115
### Changed
1216

1317
- 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)

packages/core/src/@types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ export interface JoseInstance {
190190
encodeJWT: (payload: JWTPayload) => Promise<string>
191191
signJWS: (payload: JWTPayload) => Promise<string>
192192
verifyJWS: (payload: string) => Promise<JWTPayload>
193+
encryptJWE: (payload: string) => Promise<string>
194+
decryptJWE: (payload: string) => Promise<string>
193195
}
194196

195197
/**

packages/core/src/actions/callback/access-token.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { formatZodError } from "@/utils.js"
12
import { AuthInternalError, OAuthProtocolError } from "@/errors.js"
23
import { OAuthAccessToken, OAuthAccessTokenErrorResponse, OAuthAccessTokenResponse } from "@/schemas.js"
34
import type { OAuthProviderCredentials } from "@/@types/index.js"
4-
import { formatZodError } from "@/utils.js"
55

66
/**
77
* Make a request to the OAuth provider to the token endpoint to exchange the authorization code provided
@@ -22,7 +22,7 @@ export const createAccessToken = async (
2222
) => {
2323
const parsed = OAuthAccessToken.safeParse({ ...oauthConfig, redirectURI, code, codeVerifier })
2424
if (!parsed.success) {
25-
const msg = formatZodError(parsed.error).toString()
25+
const msg = JSON.stringify(formatZodError(parsed.error), null, 2)
2626
throw new AuthInternalError("INVALID_OAUTH_CONFIGURATION", msg)
2727
}
2828
const { accessToken, clientId, clientSecret, code: codeParsed, redirectURI: redirectParsed } = parsed.data

packages/core/src/actions/callback/userinfo.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ export const getUserInfo = async (oauthConfig: OAuthProviderCredentials, accessT
4949
}
5050
return oauthConfig?.profile ? oauthConfig.profile(json) : getDefaultUserInfo(json)
5151
} catch (error) {
52-
if(isOAuthProtocolError(error)) {
52+
if (isOAuthProtocolError(error)) {
5353
throw error
5454
}
55-
if(isNativeError(error)) {
55+
if (isNativeError(error)) {
5656
throw new OAuthProtocolError("invalid_request", error.message, "", { cause: error })
5757
}
5858
throw new OAuthProtocolError("invalid_request", "Failed to fetch user information.", "", { cause: error })

packages/core/src/actions/signIn/authorization.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { isValidURL } from "@/assert.js"
22
import { OAuthAuthorization } from "@/schemas.js"
3-
import { equals, formatZodError, getNormalizedOriginPath, sanitizeURL, toCastCase } from "@/utils.js"
43
import { AuthInternalError, AuthSecurityError, isAuthSecurityError } from "@/errors.js"
4+
import { equals, formatZodError, getNormalizedOriginPath, sanitizeURL, toCastCase } from "@/utils.js"
55
import type { OAuthProviderCredentials } from "@/@types/index.js"
66

77
/**
@@ -26,7 +26,6 @@ export const createAuthorizationURL = (
2626
const parsed = OAuthAuthorization.safeParse({ ...oauthConfig, redirectURI, state, codeChallenge, codeChallengeMethod })
2727
if (!parsed.success) {
2828
const msg = JSON.stringify(formatZodError(parsed.error), null, 2)
29-
console.log("OAuth Authorization URL Creation Error:", msg)
3029
throw new AuthInternalError("INVALID_OAUTH_CONFIGURATION", msg)
3130
}
3231
const { authorizeURL, ...options } = parsed.data

packages/core/src/jose.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import "dotenv/config"
2-
import { createJWT, createJWS, createDeriveKey } from "@aura-stack/jose"
3-
import { createDerivedSalt } from "./secure.js"
4-
import { AuthInternalError } from "./errors.js"
2+
import { createJWT, createJWS, createJWE, createDeriveKey } from "@aura-stack/jose"
3+
import { createDerivedSalt } from "@/secure.js"
4+
import { AuthInternalError } from "@/errors.js"
55
export type { JWTPayload } from "@aura-stack/jose/jose"
66

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

2225
const salt = process.env.AURA_AUTH_SALT ?? createDerivedSalt(secret)
23-
const { derivedKey: derivedSessionKey } = createDeriveKey(secret, salt, "session")
26+
const { derivedKey: derivedSigningKey } = createDeriveKey(secret, salt, "signing")
27+
const { derivedKey: derivedEncryptionKey } = createDeriveKey(secret, salt, "encryption")
2428
const { derivedKey: derivedCsrfTokenKey } = createDeriveKey(secret, salt, "csrfToken")
2529

26-
const { decodeJWT, encodeJWT } = createJWT(derivedSessionKey)
30+
const { decodeJWT, encodeJWT } = createJWT({ jws: derivedSigningKey, jwe: derivedEncryptionKey })
2731
const { signJWS, verifyJWS } = createJWS(derivedCsrfTokenKey)
32+
const { encryptJWE, decryptJWE } = createJWE(derivedEncryptionKey)
2833

2934
return {
3035
decodeJWT,
3136
encodeJWT,
3237
signJWS,
3338
verifyJWS,
39+
encryptJWE,
40+
decryptJWE,
3441
}
3542
}

packages/core/src/utils.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,7 @@ export const isValidRelativePath = (path: string | undefined | null): boolean =>
111111
export const onErrorHandler: RouterConfig["onError"] = (error) => {
112112
if (isRouterError(error)) {
113113
const { message, status, statusText } = error
114-
return Response.json(
115-
{ type: "ROUTER_ERROR", code: "ROUTER_INTERNAL_ERROR", message },
116-
{ status, statusText }
117-
)
114+
return Response.json({ type: "ROUTER_ERROR", code: "ROUTER_INTERNAL_ERROR", message }, { status, statusText })
118115
}
119116
if (isInvalidZodSchemaError(error)) {
120117
return Response.json({ type: "ROUTER_ERROR", code: "INVALID_REQUEST", message: error.errors }, { status: 422 })

packages/jose/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
88

99
## [Unreleased]
1010

11+
### Added
12+
13+
- 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)
14+
1115
---
1216

1317
## [0.1.0] - 2025-12-28

packages/jose/src/assert.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ export const isFalsy = (value: unknown): boolean => {
1212
return value === null || value === undefined || value === false || value === 0 || value === "" || Number.isNaN(value)
1313
}
1414

15+
export const isObject = (value: unknown): value is Record<string, unknown> => {
16+
return typeof value === "object" && value !== null && !Array.isArray(value)
17+
}
18+
1519
export const isInvalidPayload = (payload: unknown): boolean => {
1620
return (
1721
isFalsy(payload) ||
18-
typeof payload !== "object" ||
19-
Array.isArray(payload) ||
22+
!isObject(payload) ||
2023
(typeof payload === "object" && payload !== null && !Array.isArray(payload) && Object.keys(payload).length === 0)
2124
)
2225
}

packages/jose/src/encrypt.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface EncryptedPayload {
2222
*/
2323
export const encryptJWE = async (payload: string, secret: SecretInput) => {
2424
try {
25-
if(isFalsy(payload)) {
25+
if (isFalsy(payload)) {
2626
throw new InvalidPayloadError("The payload must be a non-empty string")
2727
}
2828
const secretKey = createSecret(secret)
@@ -52,7 +52,7 @@ export const encryptJWE = async (payload: string, secret: SecretInput) => {
5252
*/
5353
export const decryptJWE = async (token: string, secret: SecretInput) => {
5454
try {
55-
if(isFalsy(token)) {
55+
if (isFalsy(token)) {
5656
throw new InvalidPayloadError("The token must be a non-empty string")
5757
}
5858
const secretKey = createSecret(secret)

0 commit comments

Comments
 (0)