From d97b3612eb03ec2122f8178344cd8634373badc6 Mon Sep 17 00:00:00 2001 From: git-veak Date: Tue, 20 Aug 2024 19:30:43 +0200 Subject: [PATCH 01/12] feat: save and access JWT token in Browser Cookies for Authentication --- .../src/utils/prepare-project.ts | 2 +- packages/core/js-sdk/src/auth/index.ts | 28 ++++++---- packages/core/js-sdk/src/client.ts | 51 ++++++++++++++++++- packages/core/js-sdk/src/types.ts | 10 +++- .../core/types/src/common/config-module.ts | 17 +++++++ .../core/utils/src/common/define-config.ts | 2 + .../framework/framework/src/config/config.ts | 1 + .../framework/framework/src/config/types.ts | 17 +++++++ .../middlewares/authenticate-middleware.ts | 6 +++ .../[actor_type]/[auth_provider]/route.ts | 9 +++- packages/medusa/src/api/auth/cookie/route.ts | 42 +++++++++++++++ 11 files changed, 169 insertions(+), 16 deletions(-) create mode 100644 packages/medusa/src/api/auth/cookie/route.ts diff --git a/packages/cli/create-medusa-app/src/utils/prepare-project.ts b/packages/cli/create-medusa-app/src/utils/prepare-project.ts index 4de4495504cd7..933f6d026cf60 100644 --- a/packages/cli/create-medusa-app/src/utils/prepare-project.ts +++ b/packages/cli/create-medusa-app/src/utils/prepare-project.ts @@ -70,7 +70,7 @@ export default async ({ let inviteToken: string | undefined = undefined // add environment variables - let env = `MEDUSA_ADMIN_ONBOARDING_TYPE=${onboardingType}${EOL}STORE_CORS=${STORE_CORS}${EOL}ADMIN_CORS=${ADMIN_CORS}${EOL}AUTH_CORS=${AUTH_CORS}${EOL}REDIS_URL=${DEFAULT_REDIS_URL}${EOL}JWT_SECRET=supersecret${EOL}COOKIE_SECRET=supersecret` + let env = `MEDUSA_ADMIN_ONBOARDING_TYPE=${onboardingType}${EOL}STORE_CORS=${STORE_CORS}${EOL}ADMIN_CORS=${ADMIN_CORS}${EOL}AUTH_CORS=${AUTH_CORS}${EOL}REDIS_URL=${DEFAULT_REDIS_URL}${EOL}JWT_SECRET=supersecret${EOL}JWT_TOKEN_STORAGE_KEY=medusa_auth_token${EOL}COOKIE_SECRET=supersecret` if (!skipDb) { env += `${EOL}DATABASE_URL=${dbConnectionString}` diff --git a/packages/core/js-sdk/src/auth/index.ts b/packages/core/js-sdk/src/auth/index.ts index c3bf8469b2414..d6a63c73b46bb 100644 --- a/packages/core/js-sdk/src/auth/index.ts +++ b/packages/core/js-sdk/src/auth/index.ts @@ -1,5 +1,5 @@ import { Client } from "../client" -import { Config } from "../types" +import { AuthActor, AuthMethod, AuthPayload, AuthResponse, Config } from "../types" export class Auth { private client: Client @@ -11,18 +11,26 @@ export class Auth { } login = async ( - actor: "customer" | "user", - method: "emailpass", - payload: { email: string; password: string } + actor: AuthActor, + method: AuthMethod, + payload: AuthPayload ) => { - const { token } = await this.client.fetch<{ token: string }>( - `/auth/${actor}/${method}`, + const response = await this.client.fetch( + `/auth/${actor}/${method}?redirect=false`, // set redirect to false, to disable automatic redirect -> otherwise cors error { method: "POST", body: payload, } ) + // Redirect to 3rd Party Login + if ("location" in response) { + window.location.href = response.location + return response.location + } + + const { token } = response as { token: string } + // By default we just set the token in memory, if configured to use sessions we convert it into session storage instead. if (this.config?.auth?.type === "session") { await this.client.fetch("/auth/session", { @@ -47,11 +55,11 @@ export class Auth { } create = async ( - actor: "customer" | "user", - method: "emailpass", - payload: { email: string; password: string } + actor: AuthActor, + method: AuthMethod, + payload: AuthPayload ): Promise<{ token: string }> => { - return await this.client.fetch(`/auth/${actor}/${method}`, { + return await this.client.fetch(`/auth/${actor}/${method}?redirect=false`, { method: "POST", body: payload, }) diff --git a/packages/core/js-sdk/src/client.ts b/packages/core/js-sdk/src/client.ts index fd12db8634aa9..59b447ebbfc8b 100644 --- a/packages/core/js-sdk/src/client.ts +++ b/packages/core/js-sdk/src/client.ts @@ -11,6 +11,14 @@ const hasStorage = (storage: "localStorage" | "sessionStorage") => { return false } +const hasCookies_ = (): boolean => { + if (hasStorage("localStorage") && typeof document !== "undefined") { + return "cookie" in document + } + + return false +}; + const toBase64 = (str: string) => { if (typeof window !== "undefined") { return window.btoa(str) @@ -46,6 +54,11 @@ const normalizeRequest = ( } const normalizeResponse = async (resp: Response, reqHeaders: Headers) => { + if (resp.status === 301 || resp.status === 302 || resp.status === 303) { + // Return the location URL for redirect handling + return { location: resp.headers.get('location')! } + } + if (resp.status >= 300) { const jsonError = (await resp.json().catch(() => ({}))) as { message?: string @@ -134,6 +147,15 @@ export class Client { window.sessionStorage.removeItem(storageKey) break } + case "cookie": { + this.fetch( + `/auth/cookie`, + { + method: "DELETE" + } + ) + break + } case "memory": { this.token = "" break @@ -192,7 +214,10 @@ export class Client { // Any non-request errors (eg. invalid JSON in the response) will be thrown as-is. return await fetch( normalizedInput, - normalizeRequest(init, headers, this.config) + { + ...normalizeRequest(init, headers, this.config), + credentials: "include" + } ).then((resp) => { this.logger.debug(`Received response with status ${resp.status}\n`) return normalizeResponse(resp, headers) @@ -235,6 +260,18 @@ export class Client { window.sessionStorage.setItem(storageKey, token) break } + case "cookie": { + this.fetch( + `/auth/cookie`, + { + method: "POST", + body: { + authToken: token + } + } + ) + break + } case "memory": { this.token = token break @@ -251,6 +288,14 @@ export class Client { case "session": { return window.sessionStorage.getItem(storageKey) } + case "cookie": { + const cookieValue = document.cookie + .split("; ") + .find((row) => row.startsWith(`${storageKey}=`)) + ?.split("=")[1] + + return cookieValue ? cookieValue : null; + } case "memory": { return this.token } @@ -262,6 +307,7 @@ export class Client { protected getTokenStorageInfo_ = () => { const hasLocal = hasStorage("localStorage") const hasSession = hasStorage("sessionStorage") + const hasCookies = hasCookies_() const storageMethod = this.config.auth?.jwtTokenStorageMethod || (hasLocal ? "local" : "memory") @@ -274,6 +320,9 @@ export class Client { if (!hasSession && storageMethod === "session") { throw new Error("Session JWT storage is only available in the browser") } + if (!hasCookies && storageMethod === "cookie") { + throw new Error("Cookie JWT storage is only available in the browser") + } return { storageMethod, diff --git a/packages/core/js-sdk/src/types.ts b/packages/core/js-sdk/src/types.ts index f9eb72a530ee8..abe1f7c15a862 100644 --- a/packages/core/js-sdk/src/types.ts +++ b/packages/core/js-sdk/src/types.ts @@ -1,3 +1,9 @@ +export type AuthResponse = { token: string } | { location: string } + +export type AuthActor = "customer" | "user" | (string & {}) +export type AuthMethod = "emailpass" | (string & {}) +export type AuthPayload = { email: string; password: string } | (Object & {}) + export type Logger = { error: (...messages: string[]) => void warn: (...messages: string[]) => void @@ -13,7 +19,7 @@ export type Config = { auth?: { type?: "jwt" | "session" jwtTokenStorageKey?: string - jwtTokenStorageMethod?: "local" | "session" | "memory" + jwtTokenStorageMethod?: "local" | "session" | "memory" | "cookie" } logger?: Logger debug?: boolean @@ -36,4 +42,4 @@ export type FetchArgs = Omit & { export type ClientFetch = ( input: FetchInput, init?: FetchArgs -) => Promise +) => Promise \ No newline at end of file diff --git a/packages/core/types/src/common/config-module.ts b/packages/core/types/src/common/config-module.ts index 28f1e4b799684..4d32e66211a8b 100644 --- a/packages/core/types/src/common/config-module.ts +++ b/packages/core/types/src/common/config-module.ts @@ -467,6 +467,23 @@ export type ProjectConfigOptions = { * ``` */ jwtExpiresIn?: string + /** + * The storage Key for the JWT token. If not provided, the default value is `medusa_auth_token`. + * + * @example + * ```js title="medusa-config.js" + * module.exports = defineConfig({ + * projectConfig: { + * http: { + * jwtTokenStorageKey: "auth_token" + * } + * // ... + * }, + * // ... + * }) + * ``` + */ + jwtTokenStorageKey?: string /** * A random string used to create cookie tokens in the http layer. Although this configuration option is not required, it’s highly recommended to set it for better security. * diff --git a/packages/core/utils/src/common/define-config.ts b/packages/core/utils/src/common/define-config.ts index d7410fc95c36c..e20e1ac8138a3 100644 --- a/packages/core/utils/src/common/define-config.ts +++ b/packages/core/utils/src/common/define-config.ts @@ -2,6 +2,7 @@ import { ConfigModule } from "@medusajs/types" import { Modules } from "../modules-sdk/definition" const DEFAULT_SECRET = "supersecret" +const DEFAULT_STORAGE_KEY = "medusa_auth_token" const DEFAULT_ADMIN_URL = "http://localhost:9000" const DEFAULT_STORE_CORS = "http://localhost:8000" const DEFAULT_DATABASE_URL = "postgres://localhost/medusa-starter-default" @@ -30,6 +31,7 @@ export function defineConfig(config: Partial = {}): ConfigModule { adminCors: process.env.ADMIN_CORS || DEFAULT_ADMIN_CORS, authCors: process.env.AUTH_CORS || DEFAULT_ADMIN_CORS, jwtSecret: process.env.JWT_SECRET || DEFAULT_SECRET, + jwtTokenStorageKey: process.env.JWT_TOKEN_STORAGE_KEY || DEFAULT_STORAGE_KEY, cookieSecret: process.env.COOKIE_SECRET || DEFAULT_SECRET, ...http, }, diff --git a/packages/framework/framework/src/config/config.ts b/packages/framework/framework/src/config/config.ts index 90e2f506d2235..cc0a28275b70f 100644 --- a/packages/framework/framework/src/config/config.ts +++ b/packages/framework/framework/src/config/config.ts @@ -76,6 +76,7 @@ export class ConfigManager { {}) as ConfigModule["projectConfig"]["http"] http.jwtExpiresIn = http?.jwtExpiresIn ?? "1d" + http.jwtTokenStorageKey = http.jwtTokenStorageKey ?? "medusa_auth_token" http.authCors = http.authCors ?? "" http.storeCors = http.storeCors ?? "" http.adminCors = http.adminCors ?? "" diff --git a/packages/framework/framework/src/config/types.ts b/packages/framework/framework/src/config/types.ts index 8240c561589bb..a71c65607d424 100644 --- a/packages/framework/framework/src/config/types.ts +++ b/packages/framework/framework/src/config/types.ts @@ -449,6 +449,23 @@ export type ProjectConfigOptions = { * ``` */ jwtExpiresIn?: string + /** + * The storage Key for the JWT token. If not provided, the default value is `medusa_auth_token`. + * + * @example + * ```js title="medusa-config.js" + * module.exports = defineConfig({ + * projectConfig: { + * http: { + * jwtTokenStorageKey: "auth_token" + * } + * // ... + * }, + * // ... + * }) + * ``` + */ + jwtTokenStorageKey?: string /** * A random string used to create cookie tokens in the http layer. Although this configuration option is not required, it’s highly recommended to set it for better security. * diff --git a/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts b/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts index ee8c7d7713c18..1033970a769f7 100644 --- a/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts +++ b/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts @@ -72,6 +72,12 @@ export const authenticate = ( ContainerRegistrationKeys.CONFIG_MODULE ) + const authToken = req.cookies[http.jwtTokenStorageKey!] + + if (authToken) { + req.headers['authorization'] = `Bearer ${authToken}` + } + authContext = getAuthContextFromJwtToken( req.headers.authorization, http.jwtSecret!, diff --git a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/route.ts b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/route.ts index 4594936679979..dc5b9cd794093 100644 --- a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/route.ts +++ b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/route.ts @@ -13,6 +13,7 @@ import { MedusaRequest, MedusaResponse } from "../../../../types/routing" export const GET = async (req: MedusaRequest, res: MedusaResponse) => { const { actor_type, auth_provider } = req.params + const shouldRedirect = req.query.redirect === 'true' || req.query.redirect === undefined const config: ConfigModule = req.scope.resolve( ContainerRegistrationKeys.CONFIG_MODULE ) @@ -47,7 +48,11 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { ) if (location) { - res.redirect(location) + if (shouldRedirect) { + res.redirect(location) + } else { + res.status(200).json({ location }) + } return } @@ -90,4 +95,4 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { export const POST = async (req: MedusaRequest, res: MedusaResponse) => { await GET(req, res) -} +} \ No newline at end of file diff --git a/packages/medusa/src/api/auth/cookie/route.ts b/packages/medusa/src/api/auth/cookie/route.ts new file mode 100644 index 0000000000000..a578eb8f7a994 --- /dev/null +++ b/packages/medusa/src/api/auth/cookie/route.ts @@ -0,0 +1,42 @@ +import { + MedusaRequest, + MedusaResponse, +} from "../../../types/routing" +import { MedusaError } from "@medusajs/utils" + +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const { jwtTokenStorageKey } = req.scope.resolve("configModule").projectConfig.http + const { authToken } = req.body as { authToken: string } + + if (!authToken) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Missing authToken from Body." + ) + } + + res.cookie(jwtTokenStorageKey, authToken, { + httpOnly: true, + secure: true, + }) + + res.status(200).json({ message: 'Saved Token to Browser Cookies.' }) +} + +export const DELETE = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const { jwtTokenStorageKey } = req.scope.resolve("configModule").projectConfig.http + + res.cookie(jwtTokenStorageKey as string, "", { + httpOnly: true, + secure: true, + expires: new Date(0) + }) + + res.status(200).json({ message: 'Logged out successfully' }); +} From c8aa6d916ccff2ca7fb15530efd3f861fb576fec Mon Sep 17 00:00:00 2001 From: Veak <51446230+v0eak@users.noreply.github.com> Date: Tue, 20 Aug 2024 21:34:54 +0200 Subject: [PATCH 02/12] fix: cast jwtTokenStorageKey as string in route --- packages/medusa/src/api/auth/cookie/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/medusa/src/api/auth/cookie/route.ts b/packages/medusa/src/api/auth/cookie/route.ts index a578eb8f7a994..1b5d9aa256023 100644 --- a/packages/medusa/src/api/auth/cookie/route.ts +++ b/packages/medusa/src/api/auth/cookie/route.ts @@ -18,7 +18,7 @@ export const POST = async ( ) } - res.cookie(jwtTokenStorageKey, authToken, { + res.cookie(jwtTokenStorageKey as string, authToken, { httpOnly: true, secure: true, }) From e6654b69481f53574b687a0f01b3bcf598a26567 Mon Sep 17 00:00:00 2001 From: Veak <51446230+v0eak@users.noreply.github.com> Date: Wed, 21 Aug 2024 10:26:46 +0200 Subject: [PATCH 03/12] fix: only pass credentials when storageMethod is set to "cookie" --- packages/core/js-sdk/src/client.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/js-sdk/src/client.ts b/packages/core/js-sdk/src/client.ts index 59b447ebbfc8b..579cfdf376650 100644 --- a/packages/core/js-sdk/src/client.ts +++ b/packages/core/js-sdk/src/client.ts @@ -179,6 +179,7 @@ export class Client { return async (input: FetchInput, init?: FetchArgs) => { // We always want to fetch the up-to-date JWT token before firing off a request. const headers = new Headers(defaultHeaders) + const { storageMethod} = this.getTokenStorageInfo_() const customHeaders = { ...this.config.globalHeaders, ...this.getJwtHeader_(), @@ -216,7 +217,7 @@ export class Client { normalizedInput, { ...normalizeRequest(init, headers, this.config), - credentials: "include" + ...(storageMethod === "cookie" ? { credentials: "include" } : {}) } ).then((resp) => { this.logger.debug(`Received response with status ${resp.status}\n`) From 8a151ae29f5fd9d2a9a3eb264368f637c80e680f Mon Sep 17 00:00:00 2001 From: git-veak Date: Wed, 21 Aug 2024 18:54:22 +0200 Subject: [PATCH 04/12] feat: save and access CSRF token in Browser Cookies for Security Layer --- packages/core/js-sdk/src/auth/index.ts | 17 +++++-- packages/core/js-sdk/src/client.ts | 47 ++++++++++++++++--- packages/core/js-sdk/src/types.ts | 3 +- .../core/types/src/common/config-module.ts | 17 +++++++ .../core/utils/src/common/define-config.ts | 2 + .../framework/framework/src/config/config.ts | 1 + .../framework/framework/src/config/types.ts | 17 +++++++ .../middlewares/authenticate-middleware.ts | 5 ++ .../[auth_provider]/callback/route.ts | 21 ++++++--- packages/medusa/src/api/auth/cookie/route.ts | 24 ++++++---- 10 files changed, 128 insertions(+), 26 deletions(-) diff --git a/packages/core/js-sdk/src/auth/index.ts b/packages/core/js-sdk/src/auth/index.ts index d6a63c73b46bb..ca7a423660a4f 100644 --- a/packages/core/js-sdk/src/auth/index.ts +++ b/packages/core/js-sdk/src/auth/index.ts @@ -29,19 +29,27 @@ export class Auth { return response.location } - const { token } = response as { token: string } + // IMPORTANT: The below code is NOT executed if the the authentication method + // is provided a successRedirectUrl in the medusa-config options + + const { authToken } = response // By default we just set the token in memory, if configured to use sessions we convert it into session storage instead. if (this.config?.auth?.type === "session") { await this.client.fetch("/auth/session", { method: "POST", - headers: { Authorization: `Bearer ${token}` }, + headers: { Authorization: `Bearer ${authToken}` }, }) + + // This will delete the csrf-Token as it is not need with session based Authentication + this.client.clearCsrfToken() } else { - this.client.setToken(token) + this.client.setToken(authToken) + + // No need to set the csrf-Token here, as it is done in the callback route } - return token + return authToken } logout = async () => { @@ -52,6 +60,7 @@ export class Auth { } this.client.clearToken() + this.client.clearCsrfToken() } create = async ( diff --git a/packages/core/js-sdk/src/client.ts b/packages/core/js-sdk/src/client.ts index 579cfdf376650..44155c7763cbc 100644 --- a/packages/core/js-sdk/src/client.ts +++ b/packages/core/js-sdk/src/client.ts @@ -132,11 +132,19 @@ export class Client { this.setToken_(token) } + setCsrfToken(token: string) { + this.setCsrfToken_(token) + } + clearToken() { this.clearToken_() } - protected clearToken_() { + clearCsrfToken() { + this.clearCsrfToken_() + } + + protected async clearToken_() { const { storageMethod, storageKey } = this.getTokenStorageInfo_() switch (storageMethod) { case "local": { @@ -148,8 +156,8 @@ export class Client { break } case "cookie": { - this.fetch( - `/auth/cookie`, + await this.fetch( + `/auth/cookie?storageKey=${storageKey}`, { method: "DELETE" } @@ -250,7 +258,7 @@ export class Client { return token ? { Authorization: `Bearer ${token}` } : {} } - protected setToken_ = (token: string) => { + protected setToken_ = async (token: string) => { const { storageMethod, storageKey } = this.getTokenStorageInfo_() switch (storageMethod) { case "local": { @@ -262,12 +270,13 @@ export class Client { break } case "cookie": { - this.fetch( + await this.fetch( `/auth/cookie`, { method: "POST", body: { - authToken: token + storageKey: storageKey, + storageValue: token } } ) @@ -330,4 +339,30 @@ export class Client { storageKey, } } + + protected setCsrfToken_ = async (token: string) => { + await this.fetch( + `/auth/cookie`, + { + method: "POST", + body: { + storageKey: this.config.auth?.csrfTokenStorageKey, + storageValue: token + } + } + ) + } + + protected clearCsrfToken_ = async () => { + await this.fetch( + `/auth/cookie?storageKey=${this.config.auth?.csrfTokenStorageKey}`, + { + method: "DELETE", + } + ) + } + + protected getCSRFToken_ = () => { + return "" + } } diff --git a/packages/core/js-sdk/src/types.ts b/packages/core/js-sdk/src/types.ts index abe1f7c15a862..9acb5cd7e8ea5 100644 --- a/packages/core/js-sdk/src/types.ts +++ b/packages/core/js-sdk/src/types.ts @@ -1,4 +1,4 @@ -export type AuthResponse = { token: string } | { location: string } +export type AuthResponse = { authToken: string, csrfToken: string } | { location: string } export type AuthActor = "customer" | "user" | (string & {}) export type AuthMethod = "emailpass" | (string & {}) @@ -20,6 +20,7 @@ export type Config = { type?: "jwt" | "session" jwtTokenStorageKey?: string jwtTokenStorageMethod?: "local" | "session" | "memory" | "cookie" + csrfTokenStorageKey?: string } logger?: Logger debug?: boolean diff --git a/packages/core/types/src/common/config-module.ts b/packages/core/types/src/common/config-module.ts index c7d418b3b4c4a..5b33a55b6c32c 100644 --- a/packages/core/types/src/common/config-module.ts +++ b/packages/core/types/src/common/config-module.ts @@ -527,6 +527,23 @@ export type ProjectConfigOptions = { * ``` */ jwtTokenStorageKey?: string + /** + * The storage Key for the CSRF token. If not provided, the default value is `medusa_csrf_token`. + * + * @example + * ```js title="medusa-config.js" + * module.exports = defineConfig({ + * projectConfig: { + * http: { + * csrfTokenStorageKey: "csrf_token" + * } + * // ... + * }, + * // ... + * }) + * ``` + */ + csrfTokenStorageKey?: string /** * A random string used to create cookie tokens in the http layer. Although this configuration option is not required, it’s highly recommended to set it for better security. * diff --git a/packages/core/utils/src/common/define-config.ts b/packages/core/utils/src/common/define-config.ts index e20e1ac8138a3..7eb462d0525c2 100644 --- a/packages/core/utils/src/common/define-config.ts +++ b/packages/core/utils/src/common/define-config.ts @@ -3,6 +3,7 @@ import { Modules } from "../modules-sdk/definition" const DEFAULT_SECRET = "supersecret" const DEFAULT_STORAGE_KEY = "medusa_auth_token" +const DEFAULT_CSRF_STORAGE_KEY = "medusa_csrf_token" const DEFAULT_ADMIN_URL = "http://localhost:9000" const DEFAULT_STORE_CORS = "http://localhost:8000" const DEFAULT_DATABASE_URL = "postgres://localhost/medusa-starter-default" @@ -32,6 +33,7 @@ export function defineConfig(config: Partial = {}): ConfigModule { authCors: process.env.AUTH_CORS || DEFAULT_ADMIN_CORS, jwtSecret: process.env.JWT_SECRET || DEFAULT_SECRET, jwtTokenStorageKey: process.env.JWT_TOKEN_STORAGE_KEY || DEFAULT_STORAGE_KEY, + csrfTokenStorageKey: process.env.CSRF_TOKEN_STORAGE_KEY || DEFAULT_CSRF_STORAGE_KEY, cookieSecret: process.env.COOKIE_SECRET || DEFAULT_SECRET, ...http, }, diff --git a/packages/framework/framework/src/config/config.ts b/packages/framework/framework/src/config/config.ts index cc0a28275b70f..89fe530473777 100644 --- a/packages/framework/framework/src/config/config.ts +++ b/packages/framework/framework/src/config/config.ts @@ -77,6 +77,7 @@ export class ConfigManager { http.jwtExpiresIn = http?.jwtExpiresIn ?? "1d" http.jwtTokenStorageKey = http.jwtTokenStorageKey ?? "medusa_auth_token" + http.csrfTokenStorageKey = http.csrfTokenStorageKey ?? "medusa_csrf_token" http.authCors = http.authCors ?? "" http.storeCors = http.storeCors ?? "" http.adminCors = http.adminCors ?? "" diff --git a/packages/framework/framework/src/config/types.ts b/packages/framework/framework/src/config/types.ts index 79cca8cfb838a..9792557292e86 100644 --- a/packages/framework/framework/src/config/types.ts +++ b/packages/framework/framework/src/config/types.ts @@ -526,6 +526,23 @@ export type ProjectConfigOptions = { * ``` */ jwtTokenStorageKey?: string + /** + * The storage Key for the CSRF token. If not provided, the default value is `medusa_csrf_token`. + * + * @example + * ```js title="medusa-config.js" + * module.exports = defineConfig({ + * projectConfig: { + * http: { + * csrfTokenStorageKey: "csrf_token" + * } + * // ... + * }, + * // ... + * }) + * ``` + */ + csrfTokenStorageKey?: string /** * A random string used to create cookie tokens in the http layer. Although this configuration option is not required, it’s highly recommended to set it for better security. * diff --git a/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts b/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts index 1033970a769f7..df2bc6e1d7b65 100644 --- a/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts +++ b/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts @@ -73,6 +73,11 @@ export const authenticate = ( ) const authToken = req.cookies[http.jwtTokenStorageKey!] + const csrfToken = req.headers["x-csrf-token"] + + console.log(csrfToken) + + // TODO: verify the CSRF Token here, if it is not verifyable, throw for missing authentication if (authToken) { req.headers['authorization'] = `Bearer ${authToken}` diff --git a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts index 82e44d01c1254..cbd4c472cbd48 100644 --- a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts +++ b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts @@ -9,6 +9,7 @@ import { ModuleRegistrationName, generateJwtToken, } from "@medusajs/utils" +import crypto from "node:crypto" import { MedusaRequest, MedusaResponse } from "../../../../../types/routing" export const GET = async (req: MedusaRequest, res: MedusaResponse) => { @@ -53,8 +54,8 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { ContainerRegistrationKeys.CONFIG_MODULE ).projectConfig - const { jwtSecret, jwtExpiresIn } = http - const token = generateJwtToken( + const { jwtSecret, jwtExpiresIn, jwtTokenStorageKey, csrfTokenStorageKey } = http + const authToken = generateJwtToken( { actor_id: entityId ?? "", actor_type, @@ -71,14 +72,22 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { } ) + const csrfToken = crypto.randomBytes(32).toString("hex") + + res.cookie(csrfTokenStorageKey as string, csrfToken) + + // TODO: save the csrf Token in cache + if (successRedirectUrl) { - const url = new URL(successRedirectUrl!) - url.searchParams.append("access_token", token) + res.cookie(jwtTokenStorageKey as string, authToken, { + httpOnly: true, + secure: true, + }) - return res.redirect(url.toString()) + return res.redirect(successRedirectUrl) } - return res.json({ token }) + return res.json({ authToken, csrfToken }) } throw new MedusaError( diff --git a/packages/medusa/src/api/auth/cookie/route.ts b/packages/medusa/src/api/auth/cookie/route.ts index 1b5d9aa256023..d12f2e40911e6 100644 --- a/packages/medusa/src/api/auth/cookie/route.ts +++ b/packages/medusa/src/api/auth/cookie/route.ts @@ -8,35 +8,41 @@ export const POST = async ( req: MedusaRequest, res: MedusaResponse ) => { - const { jwtTokenStorageKey } = req.scope.resolve("configModule").projectConfig.http - const { authToken } = req.body as { authToken: string } + const { storageKey, storageValue } = req.body as { storageKey: string; storageValue: string } - if (!authToken) { + if (!storageKey || !storageValue) { throw new MedusaError( MedusaError.Types.INVALID_DATA, - "Missing authToken from Body." + "Missing storageKey or storageValue in the request body." ) } - res.cookie(jwtTokenStorageKey as string, authToken, { + res.cookie(storageKey, storageValue, { httpOnly: true, secure: true, }) - res.status(200).json({ message: 'Saved Token to Browser Cookies.' }) + res.status(200).json({ message: `Saved ${storageKey}-Cookie successfully.` }) } export const DELETE = async ( req: MedusaRequest, res: MedusaResponse ) => { - const { jwtTokenStorageKey } = req.scope.resolve("configModule").projectConfig.http + const { storageKey } = req.query - res.cookie(jwtTokenStorageKey as string, "", { + if (!storageKey) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Missing storageKey in the request parameters." + ) + } + + res.cookie(storageKey as string, "", { httpOnly: true, secure: true, expires: new Date(0) }) - res.status(200).json({ message: 'Logged out successfully' }); + res.status(200).json({ message: `Removed the ${storageKey}-Cookie successfully.` }); } From a49592e52106ddce53af6cd50070f67183360e0c Mon Sep 17 00:00:00 2001 From: git-veak Date: Wed, 21 Aug 2024 18:58:24 +0200 Subject: [PATCH 05/12] fix: missing environment variable csrf_token_storage_key --- packages/cli/create-medusa-app/src/utils/prepare-project.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/create-medusa-app/src/utils/prepare-project.ts b/packages/cli/create-medusa-app/src/utils/prepare-project.ts index 933f6d026cf60..7e8684fad4269 100644 --- a/packages/cli/create-medusa-app/src/utils/prepare-project.ts +++ b/packages/cli/create-medusa-app/src/utils/prepare-project.ts @@ -70,7 +70,7 @@ export default async ({ let inviteToken: string | undefined = undefined // add environment variables - let env = `MEDUSA_ADMIN_ONBOARDING_TYPE=${onboardingType}${EOL}STORE_CORS=${STORE_CORS}${EOL}ADMIN_CORS=${ADMIN_CORS}${EOL}AUTH_CORS=${AUTH_CORS}${EOL}REDIS_URL=${DEFAULT_REDIS_URL}${EOL}JWT_SECRET=supersecret${EOL}JWT_TOKEN_STORAGE_KEY=medusa_auth_token${EOL}COOKIE_SECRET=supersecret` + let env = `MEDUSA_ADMIN_ONBOARDING_TYPE=${onboardingType}${EOL}STORE_CORS=${STORE_CORS}${EOL}ADMIN_CORS=${ADMIN_CORS}${EOL}AUTH_CORS=${AUTH_CORS}${EOL}REDIS_URL=${DEFAULT_REDIS_URL}${EOL}JWT_SECRET=supersecret${EOL}JWT_TOKEN_STORAGE_KEY=medusa_auth_token${EOL}CSRF_TOKEN_STORAGE_KEY=medusa_csrf_token${EOL}COOKIE_SECRET=supersecret` if (!skipDb) { env += `${EOL}DATABASE_URL=${dbConnectionString}` From f552a1e564bffd6d7987c83d952caf150cd3e735 Mon Sep 17 00:00:00 2001 From: git-veak Date: Wed, 21 Aug 2024 21:31:18 +0200 Subject: [PATCH 06/12] feat: implement CSRF Protection for JWT Tokens (BREAKING CHANGE: Accessing Protected Routes directly results in Unauthorized) --- .../src/http/middlewares/authenticate-middleware.ts | 12 +++++++++--- .../[actor_type]/[auth_provider]/callback/route.ts | 12 ++++++++---- .../medusa/src/api/utils/convert-jwt-expiration.ts | 11 +++++++++++ 3 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 packages/medusa/src/api/utils/convert-jwt-expiration.ts diff --git a/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts b/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts index df2bc6e1d7b65..479220eb98c19 100644 --- a/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts +++ b/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts @@ -1,4 +1,4 @@ -import { ApiKeyDTO, IApiKeyModuleService } from "@medusajs/types" +import { ApiKeyDTO, IApiKeyModuleService, ICacheService } from "@medusajs/types" import { ContainerRegistrationKeys, ModuleRegistrationName, @@ -71,13 +71,19 @@ export const authenticate = ( } = req.scope.resolve( ContainerRegistrationKeys.CONFIG_MODULE ) + const cacheService: ICacheService = req.scope.resolve( + ModuleRegistrationName.CACHE + ) const authToken = req.cookies[http.jwtTokenStorageKey!] const csrfToken = req.headers["x-csrf-token"] - console.log(csrfToken) + const csrfTokenCache = await cacheService.get(authToken) - // TODO: verify the CSRF Token here, if it is not verifyable, throw for missing authentication + if (csrfTokenCache && (csrfToken !== csrfTokenCache)) { + res.status(401).json({ message: "Unauthorized" }) + return + } if (authToken) { req.headers['authorization'] = `Bearer ${authToken}` diff --git a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts index cbd4c472cbd48..47cd26a477fdf 100644 --- a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts +++ b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts @@ -2,6 +2,7 @@ import { AuthenticationInput, ConfigModule, IAuthModuleService, + ICacheService } from "@medusajs/types" import { ContainerRegistrationKeys, @@ -11,6 +12,7 @@ import { } from "@medusajs/utils" import crypto from "node:crypto" import { MedusaRequest, MedusaResponse } from "../../../../../types/routing" +import { convertJwtExpiration } from "../../../../utils/convert-jwt-expiration" export const GET = async (req: MedusaRequest, res: MedusaResponse) => { const { actor_type, auth_provider } = req.params @@ -30,9 +32,12 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { } } - const service: IAuthModuleService = req.scope.resolve( + const authService: IAuthModuleService = req.scope.resolve( ModuleRegistrationName.AUTH ) + const cacheService: ICacheService = req.scope.resolve( + ModuleRegistrationName.CACHE + ) const authData = { url: req.url, @@ -43,7 +48,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { } as AuthenticationInput const { success, error, authIdentity, successRedirectUrl } = - await service.validateCallback(auth_provider, authData) + await authService.validateCallback(auth_provider, authData) const entityIdKey = `${actor_type}_id` const entityId = authIdentity?.app_metadata?.[entityIdKey] as @@ -75,8 +80,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { const csrfToken = crypto.randomBytes(32).toString("hex") res.cookie(csrfTokenStorageKey as string, csrfToken) - - // TODO: save the csrf Token in cache + await cacheService.set(authToken, csrfToken, convertJwtExpiration(jwtExpiresIn as string)) if (successRedirectUrl) { res.cookie(jwtTokenStorageKey as string, authToken, { diff --git a/packages/medusa/src/api/utils/convert-jwt-expiration.ts b/packages/medusa/src/api/utils/convert-jwt-expiration.ts new file mode 100644 index 0000000000000..dfbf70503885b --- /dev/null +++ b/packages/medusa/src/api/utils/convert-jwt-expiration.ts @@ -0,0 +1,11 @@ +export function convertJwtExpiration(expiresIn: string) { + const units = { + s: 1, + m: 60, + h: 60 * 60, + d: 24 * 60 * 60, + }; + const unit = expiresIn.slice(-1); + const value = parseInt(expiresIn.slice(0, -1)); + return value * units[unit]; +} \ No newline at end of file From 5c516d771e084bf959c3b598d1da883b09cf091b Mon Sep 17 00:00:00 2001 From: git-veak Date: Thu, 22 Aug 2024 11:17:46 +0200 Subject: [PATCH 07/12] Revert "feat: implement CSRF Protection for JWT Tokens (BREAKING CHANGE: Accessing Protected Routes directly results in Unauthorized)" This reverts commit f552a1e564bffd6d7987c83d952caf150cd3e735. --- .../src/http/middlewares/authenticate-middleware.ts | 12 +++--------- .../[actor_type]/[auth_provider]/callback/route.ts | 12 ++++-------- .../medusa/src/api/utils/convert-jwt-expiration.ts | 11 ----------- 3 files changed, 7 insertions(+), 28 deletions(-) delete mode 100644 packages/medusa/src/api/utils/convert-jwt-expiration.ts diff --git a/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts b/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts index 479220eb98c19..df2bc6e1d7b65 100644 --- a/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts +++ b/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts @@ -1,4 +1,4 @@ -import { ApiKeyDTO, IApiKeyModuleService, ICacheService } from "@medusajs/types" +import { ApiKeyDTO, IApiKeyModuleService } from "@medusajs/types" import { ContainerRegistrationKeys, ModuleRegistrationName, @@ -71,19 +71,13 @@ export const authenticate = ( } = req.scope.resolve( ContainerRegistrationKeys.CONFIG_MODULE ) - const cacheService: ICacheService = req.scope.resolve( - ModuleRegistrationName.CACHE - ) const authToken = req.cookies[http.jwtTokenStorageKey!] const csrfToken = req.headers["x-csrf-token"] - const csrfTokenCache = await cacheService.get(authToken) + console.log(csrfToken) - if (csrfTokenCache && (csrfToken !== csrfTokenCache)) { - res.status(401).json({ message: "Unauthorized" }) - return - } + // TODO: verify the CSRF Token here, if it is not verifyable, throw for missing authentication if (authToken) { req.headers['authorization'] = `Bearer ${authToken}` diff --git a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts index 47cd26a477fdf..cbd4c472cbd48 100644 --- a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts +++ b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts @@ -2,7 +2,6 @@ import { AuthenticationInput, ConfigModule, IAuthModuleService, - ICacheService } from "@medusajs/types" import { ContainerRegistrationKeys, @@ -12,7 +11,6 @@ import { } from "@medusajs/utils" import crypto from "node:crypto" import { MedusaRequest, MedusaResponse } from "../../../../../types/routing" -import { convertJwtExpiration } from "../../../../utils/convert-jwt-expiration" export const GET = async (req: MedusaRequest, res: MedusaResponse) => { const { actor_type, auth_provider } = req.params @@ -32,12 +30,9 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { } } - const authService: IAuthModuleService = req.scope.resolve( + const service: IAuthModuleService = req.scope.resolve( ModuleRegistrationName.AUTH ) - const cacheService: ICacheService = req.scope.resolve( - ModuleRegistrationName.CACHE - ) const authData = { url: req.url, @@ -48,7 +43,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { } as AuthenticationInput const { success, error, authIdentity, successRedirectUrl } = - await authService.validateCallback(auth_provider, authData) + await service.validateCallback(auth_provider, authData) const entityIdKey = `${actor_type}_id` const entityId = authIdentity?.app_metadata?.[entityIdKey] as @@ -80,7 +75,8 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { const csrfToken = crypto.randomBytes(32).toString("hex") res.cookie(csrfTokenStorageKey as string, csrfToken) - await cacheService.set(authToken, csrfToken, convertJwtExpiration(jwtExpiresIn as string)) + + // TODO: save the csrf Token in cache if (successRedirectUrl) { res.cookie(jwtTokenStorageKey as string, authToken, { diff --git a/packages/medusa/src/api/utils/convert-jwt-expiration.ts b/packages/medusa/src/api/utils/convert-jwt-expiration.ts deleted file mode 100644 index dfbf70503885b..0000000000000 --- a/packages/medusa/src/api/utils/convert-jwt-expiration.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function convertJwtExpiration(expiresIn: string) { - const units = { - s: 1, - m: 60, - h: 60 * 60, - d: 24 * 60 * 60, - }; - const unit = expiresIn.slice(-1); - const value = parseInt(expiresIn.slice(0, -1)); - return value * units[unit]; -} \ No newline at end of file From 8667f478f04d879cf6392a9c01ee6477d73635b3 Mon Sep 17 00:00:00 2001 From: git-veak Date: Thu, 22 Aug 2024 11:19:11 +0200 Subject: [PATCH 08/12] Revert "fix: missing environment variable csrf_token_storage_key" This reverts commit a49592e52106ddce53af6cd50070f67183360e0c. --- packages/cli/create-medusa-app/src/utils/prepare-project.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/create-medusa-app/src/utils/prepare-project.ts b/packages/cli/create-medusa-app/src/utils/prepare-project.ts index 7e8684fad4269..933f6d026cf60 100644 --- a/packages/cli/create-medusa-app/src/utils/prepare-project.ts +++ b/packages/cli/create-medusa-app/src/utils/prepare-project.ts @@ -70,7 +70,7 @@ export default async ({ let inviteToken: string | undefined = undefined // add environment variables - let env = `MEDUSA_ADMIN_ONBOARDING_TYPE=${onboardingType}${EOL}STORE_CORS=${STORE_CORS}${EOL}ADMIN_CORS=${ADMIN_CORS}${EOL}AUTH_CORS=${AUTH_CORS}${EOL}REDIS_URL=${DEFAULT_REDIS_URL}${EOL}JWT_SECRET=supersecret${EOL}JWT_TOKEN_STORAGE_KEY=medusa_auth_token${EOL}CSRF_TOKEN_STORAGE_KEY=medusa_csrf_token${EOL}COOKIE_SECRET=supersecret` + let env = `MEDUSA_ADMIN_ONBOARDING_TYPE=${onboardingType}${EOL}STORE_CORS=${STORE_CORS}${EOL}ADMIN_CORS=${ADMIN_CORS}${EOL}AUTH_CORS=${AUTH_CORS}${EOL}REDIS_URL=${DEFAULT_REDIS_URL}${EOL}JWT_SECRET=supersecret${EOL}JWT_TOKEN_STORAGE_KEY=medusa_auth_token${EOL}COOKIE_SECRET=supersecret` if (!skipDb) { env += `${EOL}DATABASE_URL=${dbConnectionString}` From 3222ed3b724bdce7d284941581494b3d8a5d7185 Mon Sep 17 00:00:00 2001 From: git-veak Date: Thu, 22 Aug 2024 11:19:29 +0200 Subject: [PATCH 09/12] Revert "feat: save and access CSRF token in Browser Cookies for Security Layer" This reverts commit 8a151ae29f5fd9d2a9a3eb264368f637c80e680f. --- packages/core/js-sdk/src/auth/index.ts | 17 ++----- packages/core/js-sdk/src/client.ts | 47 +++---------------- packages/core/js-sdk/src/types.ts | 3 +- .../core/types/src/common/config-module.ts | 17 ------- .../core/utils/src/common/define-config.ts | 2 - .../framework/framework/src/config/config.ts | 1 - .../framework/framework/src/config/types.ts | 17 ------- .../middlewares/authenticate-middleware.ts | 5 -- .../[auth_provider]/callback/route.ts | 21 +++------ packages/medusa/src/api/auth/cookie/route.ts | 24 ++++------ 10 files changed, 26 insertions(+), 128 deletions(-) diff --git a/packages/core/js-sdk/src/auth/index.ts b/packages/core/js-sdk/src/auth/index.ts index ca7a423660a4f..d6a63c73b46bb 100644 --- a/packages/core/js-sdk/src/auth/index.ts +++ b/packages/core/js-sdk/src/auth/index.ts @@ -29,27 +29,19 @@ export class Auth { return response.location } - // IMPORTANT: The below code is NOT executed if the the authentication method - // is provided a successRedirectUrl in the medusa-config options - - const { authToken } = response + const { token } = response as { token: string } // By default we just set the token in memory, if configured to use sessions we convert it into session storage instead. if (this.config?.auth?.type === "session") { await this.client.fetch("/auth/session", { method: "POST", - headers: { Authorization: `Bearer ${authToken}` }, + headers: { Authorization: `Bearer ${token}` }, }) - - // This will delete the csrf-Token as it is not need with session based Authentication - this.client.clearCsrfToken() } else { - this.client.setToken(authToken) - - // No need to set the csrf-Token here, as it is done in the callback route + this.client.setToken(token) } - return authToken + return token } logout = async () => { @@ -60,7 +52,6 @@ export class Auth { } this.client.clearToken() - this.client.clearCsrfToken() } create = async ( diff --git a/packages/core/js-sdk/src/client.ts b/packages/core/js-sdk/src/client.ts index 44155c7763cbc..579cfdf376650 100644 --- a/packages/core/js-sdk/src/client.ts +++ b/packages/core/js-sdk/src/client.ts @@ -132,19 +132,11 @@ export class Client { this.setToken_(token) } - setCsrfToken(token: string) { - this.setCsrfToken_(token) - } - clearToken() { this.clearToken_() } - clearCsrfToken() { - this.clearCsrfToken_() - } - - protected async clearToken_() { + protected clearToken_() { const { storageMethod, storageKey } = this.getTokenStorageInfo_() switch (storageMethod) { case "local": { @@ -156,8 +148,8 @@ export class Client { break } case "cookie": { - await this.fetch( - `/auth/cookie?storageKey=${storageKey}`, + this.fetch( + `/auth/cookie`, { method: "DELETE" } @@ -258,7 +250,7 @@ export class Client { return token ? { Authorization: `Bearer ${token}` } : {} } - protected setToken_ = async (token: string) => { + protected setToken_ = (token: string) => { const { storageMethod, storageKey } = this.getTokenStorageInfo_() switch (storageMethod) { case "local": { @@ -270,13 +262,12 @@ export class Client { break } case "cookie": { - await this.fetch( + this.fetch( `/auth/cookie`, { method: "POST", body: { - storageKey: storageKey, - storageValue: token + authToken: token } } ) @@ -339,30 +330,4 @@ export class Client { storageKey, } } - - protected setCsrfToken_ = async (token: string) => { - await this.fetch( - `/auth/cookie`, - { - method: "POST", - body: { - storageKey: this.config.auth?.csrfTokenStorageKey, - storageValue: token - } - } - ) - } - - protected clearCsrfToken_ = async () => { - await this.fetch( - `/auth/cookie?storageKey=${this.config.auth?.csrfTokenStorageKey}`, - { - method: "DELETE", - } - ) - } - - protected getCSRFToken_ = () => { - return "" - } } diff --git a/packages/core/js-sdk/src/types.ts b/packages/core/js-sdk/src/types.ts index 9acb5cd7e8ea5..abe1f7c15a862 100644 --- a/packages/core/js-sdk/src/types.ts +++ b/packages/core/js-sdk/src/types.ts @@ -1,4 +1,4 @@ -export type AuthResponse = { authToken: string, csrfToken: string } | { location: string } +export type AuthResponse = { token: string } | { location: string } export type AuthActor = "customer" | "user" | (string & {}) export type AuthMethod = "emailpass" | (string & {}) @@ -20,7 +20,6 @@ export type Config = { type?: "jwt" | "session" jwtTokenStorageKey?: string jwtTokenStorageMethod?: "local" | "session" | "memory" | "cookie" - csrfTokenStorageKey?: string } logger?: Logger debug?: boolean diff --git a/packages/core/types/src/common/config-module.ts b/packages/core/types/src/common/config-module.ts index 5b33a55b6c32c..c7d418b3b4c4a 100644 --- a/packages/core/types/src/common/config-module.ts +++ b/packages/core/types/src/common/config-module.ts @@ -527,23 +527,6 @@ export type ProjectConfigOptions = { * ``` */ jwtTokenStorageKey?: string - /** - * The storage Key for the CSRF token. If not provided, the default value is `medusa_csrf_token`. - * - * @example - * ```js title="medusa-config.js" - * module.exports = defineConfig({ - * projectConfig: { - * http: { - * csrfTokenStorageKey: "csrf_token" - * } - * // ... - * }, - * // ... - * }) - * ``` - */ - csrfTokenStorageKey?: string /** * A random string used to create cookie tokens in the http layer. Although this configuration option is not required, it’s highly recommended to set it for better security. * diff --git a/packages/core/utils/src/common/define-config.ts b/packages/core/utils/src/common/define-config.ts index 7eb462d0525c2..e20e1ac8138a3 100644 --- a/packages/core/utils/src/common/define-config.ts +++ b/packages/core/utils/src/common/define-config.ts @@ -3,7 +3,6 @@ import { Modules } from "../modules-sdk/definition" const DEFAULT_SECRET = "supersecret" const DEFAULT_STORAGE_KEY = "medusa_auth_token" -const DEFAULT_CSRF_STORAGE_KEY = "medusa_csrf_token" const DEFAULT_ADMIN_URL = "http://localhost:9000" const DEFAULT_STORE_CORS = "http://localhost:8000" const DEFAULT_DATABASE_URL = "postgres://localhost/medusa-starter-default" @@ -33,7 +32,6 @@ export function defineConfig(config: Partial = {}): ConfigModule { authCors: process.env.AUTH_CORS || DEFAULT_ADMIN_CORS, jwtSecret: process.env.JWT_SECRET || DEFAULT_SECRET, jwtTokenStorageKey: process.env.JWT_TOKEN_STORAGE_KEY || DEFAULT_STORAGE_KEY, - csrfTokenStorageKey: process.env.CSRF_TOKEN_STORAGE_KEY || DEFAULT_CSRF_STORAGE_KEY, cookieSecret: process.env.COOKIE_SECRET || DEFAULT_SECRET, ...http, }, diff --git a/packages/framework/framework/src/config/config.ts b/packages/framework/framework/src/config/config.ts index 89fe530473777..cc0a28275b70f 100644 --- a/packages/framework/framework/src/config/config.ts +++ b/packages/framework/framework/src/config/config.ts @@ -77,7 +77,6 @@ export class ConfigManager { http.jwtExpiresIn = http?.jwtExpiresIn ?? "1d" http.jwtTokenStorageKey = http.jwtTokenStorageKey ?? "medusa_auth_token" - http.csrfTokenStorageKey = http.csrfTokenStorageKey ?? "medusa_csrf_token" http.authCors = http.authCors ?? "" http.storeCors = http.storeCors ?? "" http.adminCors = http.adminCors ?? "" diff --git a/packages/framework/framework/src/config/types.ts b/packages/framework/framework/src/config/types.ts index 9792557292e86..79cca8cfb838a 100644 --- a/packages/framework/framework/src/config/types.ts +++ b/packages/framework/framework/src/config/types.ts @@ -526,23 +526,6 @@ export type ProjectConfigOptions = { * ``` */ jwtTokenStorageKey?: string - /** - * The storage Key for the CSRF token. If not provided, the default value is `medusa_csrf_token`. - * - * @example - * ```js title="medusa-config.js" - * module.exports = defineConfig({ - * projectConfig: { - * http: { - * csrfTokenStorageKey: "csrf_token" - * } - * // ... - * }, - * // ... - * }) - * ``` - */ - csrfTokenStorageKey?: string /** * A random string used to create cookie tokens in the http layer. Although this configuration option is not required, it’s highly recommended to set it for better security. * diff --git a/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts b/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts index df2bc6e1d7b65..1033970a769f7 100644 --- a/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts +++ b/packages/framework/framework/src/http/middlewares/authenticate-middleware.ts @@ -73,11 +73,6 @@ export const authenticate = ( ) const authToken = req.cookies[http.jwtTokenStorageKey!] - const csrfToken = req.headers["x-csrf-token"] - - console.log(csrfToken) - - // TODO: verify the CSRF Token here, if it is not verifyable, throw for missing authentication if (authToken) { req.headers['authorization'] = `Bearer ${authToken}` diff --git a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts index cbd4c472cbd48..82e44d01c1254 100644 --- a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts +++ b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts @@ -9,7 +9,6 @@ import { ModuleRegistrationName, generateJwtToken, } from "@medusajs/utils" -import crypto from "node:crypto" import { MedusaRequest, MedusaResponse } from "../../../../../types/routing" export const GET = async (req: MedusaRequest, res: MedusaResponse) => { @@ -54,8 +53,8 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { ContainerRegistrationKeys.CONFIG_MODULE ).projectConfig - const { jwtSecret, jwtExpiresIn, jwtTokenStorageKey, csrfTokenStorageKey } = http - const authToken = generateJwtToken( + const { jwtSecret, jwtExpiresIn } = http + const token = generateJwtToken( { actor_id: entityId ?? "", actor_type, @@ -72,22 +71,14 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { } ) - const csrfToken = crypto.randomBytes(32).toString("hex") - - res.cookie(csrfTokenStorageKey as string, csrfToken) - - // TODO: save the csrf Token in cache - if (successRedirectUrl) { - res.cookie(jwtTokenStorageKey as string, authToken, { - httpOnly: true, - secure: true, - }) + const url = new URL(successRedirectUrl!) + url.searchParams.append("access_token", token) - return res.redirect(successRedirectUrl) + return res.redirect(url.toString()) } - return res.json({ authToken, csrfToken }) + return res.json({ token }) } throw new MedusaError( diff --git a/packages/medusa/src/api/auth/cookie/route.ts b/packages/medusa/src/api/auth/cookie/route.ts index d12f2e40911e6..1b5d9aa256023 100644 --- a/packages/medusa/src/api/auth/cookie/route.ts +++ b/packages/medusa/src/api/auth/cookie/route.ts @@ -8,41 +8,35 @@ export const POST = async ( req: MedusaRequest, res: MedusaResponse ) => { - const { storageKey, storageValue } = req.body as { storageKey: string; storageValue: string } + const { jwtTokenStorageKey } = req.scope.resolve("configModule").projectConfig.http + const { authToken } = req.body as { authToken: string } - if (!storageKey || !storageValue) { + if (!authToken) { throw new MedusaError( MedusaError.Types.INVALID_DATA, - "Missing storageKey or storageValue in the request body." + "Missing authToken from Body." ) } - res.cookie(storageKey, storageValue, { + res.cookie(jwtTokenStorageKey as string, authToken, { httpOnly: true, secure: true, }) - res.status(200).json({ message: `Saved ${storageKey}-Cookie successfully.` }) + res.status(200).json({ message: 'Saved Token to Browser Cookies.' }) } export const DELETE = async ( req: MedusaRequest, res: MedusaResponse ) => { - const { storageKey } = req.query + const { jwtTokenStorageKey } = req.scope.resolve("configModule").projectConfig.http - if (!storageKey) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Missing storageKey in the request parameters." - ) - } - - res.cookie(storageKey as string, "", { + res.cookie(jwtTokenStorageKey as string, "", { httpOnly: true, secure: true, expires: new Date(0) }) - res.status(200).json({ message: `Removed the ${storageKey}-Cookie successfully.` }); + res.status(200).json({ message: 'Logged out successfully' }); } From e82a926028ac8fda2b6c61b6aeca451a9d9143e0 Mon Sep 17 00:00:00 2001 From: git-veak Date: Thu, 22 Aug 2024 11:24:10 +0200 Subject: [PATCH 10/12] revert: feat: CSRF Protection & Token --- .../[auth_provider]/callback/route.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts index 82e44d01c1254..e5661a2042a53 100644 --- a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts +++ b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts @@ -53,8 +53,8 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { ContainerRegistrationKeys.CONFIG_MODULE ).projectConfig - const { jwtSecret, jwtExpiresIn } = http - const token = generateJwtToken( + const { jwtSecret, jwtExpiresIn, jwtTokenStorageKey } = http + const authToken = generateJwtToken( { actor_id: entityId ?? "", actor_type, @@ -73,12 +73,21 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { if (successRedirectUrl) { const url = new URL(successRedirectUrl!) - url.searchParams.append("access_token", token) + url.searchParams.append("access_token", authToken) return res.redirect(url.toString()) } - return res.json({ token }) + if (successRedirectUrl) { + res.cookie(jwtTokenStorageKey, authToken, { + httpOnly: true, + secure: true, + }) + + return res.redirect(successRedirectUrl) + } + + return res.json({ authToken }) } throw new MedusaError( From 4667bf7e8415b84a61344d1dcfe6aa1e05ad85f2 Mon Sep 17 00:00:00 2001 From: git-veak Date: Thu, 22 Aug 2024 11:34:07 +0200 Subject: [PATCH 11/12] refactor: change auth-cookie route, rename token authToken --- packages/core/js-sdk/src/auth/index.ts | 8 +++---- packages/core/js-sdk/src/client.ts | 13 ++++++----- packages/core/js-sdk/src/types.ts | 2 +- packages/medusa/src/api/auth/cookie/route.ts | 24 ++++++++++++-------- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/packages/core/js-sdk/src/auth/index.ts b/packages/core/js-sdk/src/auth/index.ts index d6a63c73b46bb..7197b50eb5656 100644 --- a/packages/core/js-sdk/src/auth/index.ts +++ b/packages/core/js-sdk/src/auth/index.ts @@ -29,19 +29,19 @@ export class Auth { return response.location } - const { token } = response as { token: string } + const { authToken } = response as { authToken: string } // By default we just set the token in memory, if configured to use sessions we convert it into session storage instead. if (this.config?.auth?.type === "session") { await this.client.fetch("/auth/session", { method: "POST", - headers: { Authorization: `Bearer ${token}` }, + headers: { Authorization: `Bearer ${authToken}` }, }) } else { - this.client.setToken(token) + this.client.setToken(authToken) } - return token + return authToken } logout = async () => { diff --git a/packages/core/js-sdk/src/client.ts b/packages/core/js-sdk/src/client.ts index 579cfdf376650..60a351a8fddd8 100644 --- a/packages/core/js-sdk/src/client.ts +++ b/packages/core/js-sdk/src/client.ts @@ -136,7 +136,7 @@ export class Client { this.clearToken_() } - protected clearToken_() { + protected async clearToken_() { const { storageMethod, storageKey } = this.getTokenStorageInfo_() switch (storageMethod) { case "local": { @@ -148,8 +148,8 @@ export class Client { break } case "cookie": { - this.fetch( - `/auth/cookie`, + await this.fetch( + `/auth/cookie?storageKey=${storageKey}`, { method: "DELETE" } @@ -250,7 +250,7 @@ export class Client { return token ? { Authorization: `Bearer ${token}` } : {} } - protected setToken_ = (token: string) => { + protected setToken_ = async (token: string) => { const { storageMethod, storageKey } = this.getTokenStorageInfo_() switch (storageMethod) { case "local": { @@ -262,12 +262,13 @@ export class Client { break } case "cookie": { - this.fetch( + await this.fetch( `/auth/cookie`, { method: "POST", body: { - authToken: token + storageKey: storageKey, + storageValue: token } } ) diff --git a/packages/core/js-sdk/src/types.ts b/packages/core/js-sdk/src/types.ts index abe1f7c15a862..22db60d7a6095 100644 --- a/packages/core/js-sdk/src/types.ts +++ b/packages/core/js-sdk/src/types.ts @@ -1,4 +1,4 @@ -export type AuthResponse = { token: string } | { location: string } +export type AuthResponse = { authToken: string } | { location: string } export type AuthActor = "customer" | "user" | (string & {}) export type AuthMethod = "emailpass" | (string & {}) diff --git a/packages/medusa/src/api/auth/cookie/route.ts b/packages/medusa/src/api/auth/cookie/route.ts index 1b5d9aa256023..d12f2e40911e6 100644 --- a/packages/medusa/src/api/auth/cookie/route.ts +++ b/packages/medusa/src/api/auth/cookie/route.ts @@ -8,35 +8,41 @@ export const POST = async ( req: MedusaRequest, res: MedusaResponse ) => { - const { jwtTokenStorageKey } = req.scope.resolve("configModule").projectConfig.http - const { authToken } = req.body as { authToken: string } + const { storageKey, storageValue } = req.body as { storageKey: string; storageValue: string } - if (!authToken) { + if (!storageKey || !storageValue) { throw new MedusaError( MedusaError.Types.INVALID_DATA, - "Missing authToken from Body." + "Missing storageKey or storageValue in the request body." ) } - res.cookie(jwtTokenStorageKey as string, authToken, { + res.cookie(storageKey, storageValue, { httpOnly: true, secure: true, }) - res.status(200).json({ message: 'Saved Token to Browser Cookies.' }) + res.status(200).json({ message: `Saved ${storageKey}-Cookie successfully.` }) } export const DELETE = async ( req: MedusaRequest, res: MedusaResponse ) => { - const { jwtTokenStorageKey } = req.scope.resolve("configModule").projectConfig.http + const { storageKey } = req.query - res.cookie(jwtTokenStorageKey as string, "", { + if (!storageKey) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Missing storageKey in the request parameters." + ) + } + + res.cookie(storageKey as string, "", { httpOnly: true, secure: true, expires: new Date(0) }) - res.status(200).json({ message: 'Logged out successfully' }); + res.status(200).json({ message: `Removed the ${storageKey}-Cookie successfully.` }); } From f4c72047bf293f394ae21810dd8b3005b44affe8 Mon Sep 17 00:00:00 2001 From: git-veak Date: Thu, 22 Aug 2024 11:48:14 +0200 Subject: [PATCH 12/12] fix: cast jwtTokenStorageKey as string --- .../auth/[actor_type]/[auth_provider]/callback/route.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts index e5661a2042a53..be22fb38e5a1b 100644 --- a/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts +++ b/packages/medusa/src/api/auth/[actor_type]/[auth_provider]/callback/route.ts @@ -72,14 +72,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { ) if (successRedirectUrl) { - const url = new URL(successRedirectUrl!) - url.searchParams.append("access_token", authToken) - - return res.redirect(url.toString()) - } - - if (successRedirectUrl) { - res.cookie(jwtTokenStorageKey, authToken, { + res.cookie(jwtTokenStorageKey as string, authToken, { httpOnly: true, secure: true, })