From e8bd1b2a42c0644dca354bfcc2c85524cd9c286c Mon Sep 17 00:00:00 2001 From: Hernan Alvarado Date: Sat, 6 Dec 2025 17:03:31 -0500 Subject: [PATCH 1/2] feat(core): add `secret` configuration option --- packages/core/src/@types/index.ts | 9 ++++++++ .../core/src/actions/callback/callback.ts | 9 ++++---- packages/core/src/actions/session/session.ts | 5 ++-- packages/core/src/actions/signOut/signOut.ts | 11 ++++----- packages/core/src/cookie.ts | 23 ++++++++++++++----- packages/core/src/index.ts | 11 ++++++--- packages/core/src/jose.ts | 18 +++++++++++++++ packages/core/src/secure.ts | 4 ++-- pnpm-lock.yaml | 1 + 9 files changed, 67 insertions(+), 24 deletions(-) diff --git a/packages/core/src/@types/index.ts b/packages/core/src/@types/index.ts index 61440de4..247c10ac 100644 --- a/packages/core/src/@types/index.ts +++ b/packages/core/src/@types/index.ts @@ -3,6 +3,7 @@ import type { OAuthIntegrations } from "@/oauth/index.js" import { OAuthAccessTokenErrorResponse, OAuthAuthorizationErrorResponse } from "@/schemas.js" import { SESSION_VERSION } from "@/actions/session/session.js" import { SerializeOptions } from "cookie" +import { createJoseInstance } from "@/jose.js" /** * Standardized user profile returned by OAuth integrations after fetching user information @@ -124,11 +125,19 @@ export interface AuthConfig { * @see https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html#name-the-__host-prefix */ cookies?: CookieOptions + /** + * Secret used to sign and verify JWT tokens for session and csrf protection. + * If not provided, it will load from the environment variable `AURA_AUTH_SECRET`, but if it + * doesn't exist, it will throw an error during the initialization of the Auth module. + */ + secret?: string } export interface AuthConfigInternal { oauth: Record, OAuthSecureConfig> cookies: CookieOptions + secret: string + jose: Awaited> } /** diff --git a/packages/core/src/actions/callback/callback.ts b/packages/core/src/actions/callback/callback.ts index c9e59688..06375ac7 100644 --- a/packages/core/src/actions/callback/callback.ts +++ b/packages/core/src/actions/callback/callback.ts @@ -1,13 +1,13 @@ import z from "zod" import { createEndpoint, createEndpointConfig, statusCode } from "@aura-stack/router" import { createCSRF } from "@/secure.js" -import { equals, isValidRelativePath, sanitizeURL } from "@/utils.js" import { cacheControl } from "@/headers.js" import { getUserInfo } from "./userinfo.js" import { AuraResponse } from "@/response.js" import { createAccessToken } from "./access-token.js" -import { SESSION_VERSION } from "../session/session.js" +import { SESSION_VERSION } from "@/actions/session/session.js" import { AuthError, ERROR_RESPONSE, isAuthError } from "@/error.js" +import { equals, isValidRelativePath, sanitizeURL } from "@/utils.js" import { OAuthAuthorizationErrorResponse, OAuthAuthorizationResponse } from "@/schemas.js" import { createSessionCookie, expireCookie, getCookie, secureCookieOptions, setCookie } from "@/cookie.js" import type { JWTPayload } from "@/jose.js" @@ -23,7 +23,7 @@ const callbackConfig = (oauth: AuthConfigInternal["oauth"]) => { }, }) } -export const callbackAction = ({ oauth: oauthIntegrations, cookies }: AuthConfigInternal) => { +export const callbackAction = ({ oauth: oauthIntegrations, cookies, jose }: AuthConfigInternal) => { return createEndpoint( "GET", "/callback/:oauth", @@ -68,7 +68,8 @@ export const callbackAction = ({ oauth: oauthIntegrations, cookies }: AuthConfig integrations: [oauth], version: SESSION_VERSION, } as never as JWTPayload, - cookieOptions + cookieOptions, + jose ) const csrfToken = await createCSRF() diff --git a/packages/core/src/actions/session/session.ts b/packages/core/src/actions/session/session.ts index a8fe016b..e7551d66 100644 --- a/packages/core/src/actions/session/session.ts +++ b/packages/core/src/actions/session/session.ts @@ -1,6 +1,5 @@ import { createEndpoint } from "@aura-stack/router" import { equals } from "@/utils.js" -import { decodeJWT } from "@/jose.js" import { AuthError } from "@/error.js" import { cacheControl } from "@/headers.js" import { expireCookie, getCookie, secureCookieOptions } from "@/cookie.js" @@ -8,12 +7,12 @@ import type { AuthConfigInternal, OAuthUserProfile, OAuthUserProfileInternal } f export const SESSION_VERSION = "v0.1.0" -export const sessionAction = ({ cookies }: AuthConfigInternal) => { +export const sessionAction = ({ cookies, jose }: AuthConfigInternal) => { return createEndpoint("GET", "/session", async (request) => { const cookieOptions = secureCookieOptions(request, cookies) try { const session = getCookie(request, "sessionToken", cookieOptions) - const decoded = (await decodeJWT(session)) as OAuthUserProfile + const decoded = (await jose.decodeJWT(session)) as OAuthUserProfile const user: OAuthUserProfileInternal = { sub: decoded.sub, email: decoded.email, diff --git a/packages/core/src/actions/signOut/signOut.ts b/packages/core/src/actions/signOut/signOut.ts index 22b67ff5..a41c0b8b 100644 --- a/packages/core/src/actions/signOut/signOut.ts +++ b/packages/core/src/actions/signOut/signOut.ts @@ -1,14 +1,13 @@ import z from "zod" import { createEndpoint, createEndpointConfig, statusCode } from "@aura-stack/router" -import { decodeJWT } from "@/jose.js" import { verifyCSRF } from "@/secure.js" import { cacheControl } from "@/headers.js" import { AuraResponse } from "@/response.js" -import { AuthConfigInternal, OAuthErrorResponse } from "@/@types/index.js" +import { getNormalizedOriginPath } from "@/utils.js" +import { createRedirectTo } from "@/actions/signIn/authorization.js" import { expireCookie, getCookie, secureCookieOptions } from "@/cookie.js" -import { createRedirectTo } from "../signIn/authorization.js" import { InvalidCsrfTokenError, InvalidRedirectToError } from "@/error.js" -import { getNormalizedOriginPath } from "@/utils.js" +import type { AuthConfigInternal, OAuthErrorResponse } from "@/@types/index.js" const config = createEndpointConfig({ schemas: { @@ -22,7 +21,7 @@ const config = createEndpointConfig({ /** * @see https://datatracker.ietf.org/doc/html/rfc7009 */ -export const signOutAction = ({ cookies }: AuthConfigInternal) => { +export const signOutAction = ({ cookies, jose }: AuthConfigInternal) => { return createEndpoint( "POST", "/signOut", @@ -39,7 +38,7 @@ export const signOutAction = ({ cookies }: AuthConfigInternal) => { throw new Error("Missing CSRF token or session token") } await verifyCSRF(csrfToken, header) - await decodeJWT(session) + await jose.decodeJWT(session) const normalizedOriginPath = getNormalizedOriginPath(request.url) const redirectTo = createRedirectTo( diff --git a/packages/core/src/cookie.ts b/packages/core/src/cookie.ts index fa454462..1168104b 100644 --- a/packages/core/src/cookie.ts +++ b/packages/core/src/cookie.ts @@ -1,8 +1,15 @@ import { parse, serialize, type SerializeOptions } from "cookie" -import { AuthError } from "./error.js" -import { isRequest } from "./assert.js" -import { encodeJWT, type JWTPayload } from "./jose.js" -import type { CookieName, CookieOptions, CookieOptionsInternal, LiteralUnion, StandardCookie } from "@/@types/index.js" +import { AuthError } from "@/error.js" +import { isRequest } from "@/assert.js" +import type { JWTPayload } from "@/jose.js" +import type { + AuthConfigInternal, + CookieName, + CookieOptions, + CookieOptionsInternal, + LiteralUnion, + StandardCookie, +} from "@/@types/index.js" export { parse } from "cookie" @@ -131,9 +138,13 @@ export const getCookie = ( * @param session - The JWT payload to be encoded in the session cookie * @returns The serialized session cookie string */ -export const createSessionCookie = async (session: JWTPayload, cookieOptions: CookieOptionsInternal) => { +export const createSessionCookie = async ( + session: JWTPayload, + cookieOptions: CookieOptionsInternal, + jose: AuthConfigInternal["jose"] +) => { try { - const encoded = await encodeJWT(session) + const encoded = await jose.encodeJWT(session) return setCookie("sessionToken", encoded, cookieOptions) } catch (error) { // @ts-ignore diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 224ec90d..40841018 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,6 +1,7 @@ import "dotenv/config" import { createRouter, type RouterConfig } from "@aura-stack/router" import { onErrorHandler } from "./utils.js" +import { createJoseInstance } from "@/jose.js" import { defaultCookieConfig } from "@/cookie.js" import { createOAuthIntegrations } from "@/oauth/index.js" import { signInAction, callbackAction, sessionAction, signOutAction, csrfTokenAction } from "@/actions/index.js" @@ -11,10 +12,14 @@ const routerConfig: RouterConfig = { onError: onErrorHandler, } -const createInternalConfig = (authConfig?: AuthConfig): AuthConfigInternal => { +const createInternalConfig = (authConfig: AuthConfig): AuthConfigInternal => { + const jose = createJoseInstance(authConfig.secret) + return { - oauth: createOAuthIntegrations(authConfig?.oauth), - cookies: authConfig?.cookies ?? defaultCookieConfig, + oauth: createOAuthIntegrations(authConfig.oauth), + cookies: authConfig.cookies ?? defaultCookieConfig, + secret: authConfig.secret ?? process.env.AURA_AUTH_SECRET!, + jose, } } diff --git a/packages/core/src/jose.ts b/packages/core/src/jose.ts index 9bfc55a9..0b1db43b 100644 --- a/packages/core/src/jose.ts +++ b/packages/core/src/jose.ts @@ -2,6 +2,7 @@ import "dotenv/config" import { createJWT, createJWS, createDeriveKey } from "@aura-stack/jose" export type { JWTPayload } from "@aura-stack/jose/jose" +/* const secretKey = process.env.AURA_AUTH_SECRET! const { derivedKey: derivedSessionKey } = createDeriveKey(secretKey, "session") @@ -9,3 +10,20 @@ const { derivedKey: derivedCsrfTokenKey } = createDeriveKey(secretKey, "csrfToke export const { decodeJWT, encodeJWT } = createJWT(derivedSessionKey) export const { signJWS, verifyJWS } = createJWS(derivedCsrfTokenKey) +*/ + +export const createJoseInstance = (secret?: string) => { + secret ??= process.env.AURA_AUTH_SECRET! + const { derivedKey: derivedSessionKey } = createDeriveKey(secret, "session") + const { derivedKey: derivedCsrfTokenKey } = createDeriveKey(secret, "csrfToken") + + const { decodeJWT, encodeJWT } = createJWT(derivedSessionKey) + const { signJWS, verifyJWS } = createJWS(derivedCsrfTokenKey) + + return { + decodeJWT, + encodeJWT, + signJWS, + verifyJWS, + } +} diff --git a/packages/core/src/secure.ts b/packages/core/src/secure.ts index 64f851e8..e2c4648d 100644 --- a/packages/core/src/secure.ts +++ b/packages/core/src/secure.ts @@ -50,8 +50,8 @@ export const verifyCSRF = async (cookie: string, header: string): Promise=20.9.0'} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 From cab2923c552216a58bca24978471ac4fd13e4458 Mon Sep 17 00:00:00 2001 From: Hernan Alvarado Date: Tue, 9 Dec 2025 17:17:57 -0500 Subject: [PATCH 2/2] fix: update nextjs app --- apps/nextjs/src/app/page.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/nextjs/src/app/page.tsx b/apps/nextjs/src/app/page.tsx index 9ece3691..99e16353 100644 --- a/apps/nextjs/src/app/page.tsx +++ b/apps/nextjs/src/app/page.tsx @@ -6,14 +6,19 @@ const getSession = async () => { const headersList = new Headers(await headers()) const session = await fetch("http://localhost:3000/auth/session", { headers: headersList, + cache: "no-store", }) const response = await session.json() return response } const getCSRFToken = async () => { + const cookieStore = await cookies() const csrfResponse = await fetch("http://localhost:3000/auth/csrfToken", { method: "GET", + headers: { + Cookie: cookieStore.toString(), + }, }) const csrfData = await csrfResponse.json() return csrfData.csrfToken @@ -36,13 +41,13 @@ const signOut = async () => { const signOutResponse = await fetch("http://localhost:3000/auth/signOut?token_type_hint=session_token", { method: "POST", headers: headersList, - credentials: "include", body: JSON.stringify({}), + cache: "no-cache", }) const response = await signOutResponse.json() if (signOutResponse.status === 202) { - cookieStore.delete("aura-stack.sessionToken") - cookieStore.delete("aura-stack.csrfToken") + cookieStore.delete("aura-auth.sessionToken") + cookieStore.delete("aura-auth.csrfToken") redirect("/") } return response