From 0deb2776ad2babf92fe60b04606ff10130e2c297 Mon Sep 17 00:00:00 2001 From: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Mon, 25 Mar 2024 08:55:21 +0100 Subject: [PATCH] feat: Add `successRedirectUrl` to auth options (#6792) --- .../admin/__snapshots__/auth.js.snap | 15 +++ integration-tests/api/__tests__/admin/auth.js | 124 ++++++++++-------- integration-tests/api/package.json | 2 +- integration-tests/helpers/admin-seeder.js | 6 +- .../helpers/create-admin-user.ts | 5 +- packages/auth/src/providers/google.ts | 31 +++-- packages/auth/src/services/auth-user.ts | 7 +- .../callback/route.ts | 25 ++-- .../route.ts | 4 +- .../medusa/src/api-v2/auth/middlewares.ts | 10 ++ packages/types/src/auth/common/provider.ts | 5 + 11 files changed, 147 insertions(+), 87 deletions(-) rename packages/medusa/src/api-v2/auth/[scope]/{[authProvider] => [auth_provider]}/callback/route.ts (71%) rename packages/medusa/src/api-v2/auth/[scope]/{[authProvider] => [auth_provider]}/route.ts (90%) diff --git a/integration-tests/api/__tests__/admin/__snapshots__/auth.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/auth.js.snap index 6943e2454f..27c839539f 100644 --- a/integration-tests/api/__tests__/admin/__snapshots__/auth.js.snap +++ b/integration-tests/api/__tests__/admin/__snapshots__/auth.js.snap @@ -14,3 +14,18 @@ Object { "updated_at": Any, } `; + +exports[`creates admin session correctly 1`] = ` +Object { + "api_token": "test_token", + "created_at": Any, + "deleted_at": null, + "email": "admin@medusa.js", + "first_name": null, + "id": "admin_user", + "last_name": null, + "metadata": null, + "role": "admin", + "updated_at": Any, +} +`; diff --git a/integration-tests/api/__tests__/admin/auth.js b/integration-tests/api/__tests__/admin/auth.js index e1b42f6517..f3395835ff 100644 --- a/integration-tests/api/__tests__/admin/auth.js +++ b/integration-tests/api/__tests__/admin/auth.js @@ -1,66 +1,78 @@ -const path = require("path") -const { Region, DiscountRule, Discount } = require("@medusajs/medusa") - -const setupServer = require("../../../environment-helpers/setup-server") const { useApi } = require("../../../environment-helpers/use-api") -const { initDb, useDb } = require("../../../environment-helpers/use-db") -const adminSeeder = require("../../../helpers/admin-seeder") -const { exportAllDeclaration } = require("@babel/types") +const { medusaIntegrationTestRunner } = require("medusa-test-utils") +const { createAdminUser } = require("../../../helpers/create-admin-user") +const { breaking } = require("../../../helpers/breaking") + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} jest.setTimeout(30000) -describe("/admin/auth", () => { - let medusaProcess - let dbConnection +medusaIntegrationTestRunner({ + env: { MEDUSA_FF_MEDUSA_V2: true }, + testSuite: ({ dbConnection, getContainer, api }) => { + let container - beforeAll(async () => { - const cwd = path.resolve(path.join(__dirname, "..", "..")) - dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd }) - - await adminSeeder(dbConnection) - }) - - afterAll(async () => { - const db = useDb() - await db.shutdown() - medusaProcess.kill() - }) - - it("creates admin session correctly", async () => { - const api = useApi() - - const response = await api - .post("/admin/auth", { - email: "admin@medusa.js", - password: "secret_password", - }) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.user.password_hash).toEqual(undefined) - expect(response.data.user).toMatchSnapshot({ - email: "admin@medusa.js", - created_at: expect.any(String), - updated_at: expect.any(String), + beforeEach(async () => { + container = getContainer() + await createAdminUser(dbConnection, adminHeaders, container) }) - }) - it("creates admin JWT token correctly", async () => { - const api = useApi() + it("creates admin session correctly", async () => { + const response = await breaking( + async () => { + return await api.post("/admin/auth", { + email: "admin@medusa.js", + password: "secret_password", + }) + }, + async () => { + return await api.post("/auth/admin/emailpass", { + email: "admin@medusa.js", + password: "secret_password", + }) + } + ) - const response = await api - .post("/admin/auth/token", { - email: "admin@medusa.js", - password: "secret_password", + expect(response.status).toEqual(200) + + const v1Result = { + user: expect.objectContaining({ + email: "admin@medusa.js", + created_at: expect.any(String), + updated_at: expect.any(String), + }), + } + + // In V2, we respond with a token instead of the user object on session creation + const v2Result = { token: expect.any(String) } + + expect(response.data).toEqual( + breaking( + () => v1Result, + () => v2Result + ) + ) + }) + + // TODO: Remove in V2, as this is no longer supported + it("creates admin JWT token correctly", async () => { + breaking(async () => { + const response = await api + .post("/admin/auth/token", { + email: "admin@medusa.js", + password: "secret_password", + }) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.access_token).toEqual(expect.any(String)) }) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.access_token).toEqual(expect.any(String)) - }) + }) + }, }) diff --git a/integration-tests/api/package.json b/integration-tests/api/package.json index 02de1d7ece..6f49980f55 100644 --- a/integration-tests/api/package.json +++ b/integration-tests/api/package.json @@ -5,7 +5,7 @@ "license": "MIT", "private": true, "scripts": { - "test:integration": "jest --silent --maxWorkers=50% --bail --detectOpenHandles --forceExit --logHeapUsage", + "test:integration": "jest --silent=false --maxWorkers=50% --bail --detectOpenHandles --forceExit --logHeapUsage", "test:integration:chunk": "jest --silent --bail --maxWorkers=50% --forceExit --testPathPattern=$(echo $CHUNKS | jq -r \".[${CHUNK}] | .[]\")", "build": "babel src -d dist --extensions \".ts,.js\"" }, diff --git a/integration-tests/helpers/admin-seeder.js b/integration-tests/helpers/admin-seeder.js index 74928ba921..25a49b64a7 100644 --- a/integration-tests/helpers/admin-seeder.js +++ b/integration-tests/helpers/admin-seeder.js @@ -4,10 +4,10 @@ const { User } = require("@medusajs/medusa/dist/models/user") module.exports = async (dataSource, data = {}) => { const manager = dataSource.manager - const buf = await Scrypt.kdf("secret_password", { logN: 1, r: 1, p: 1 }) + const buf = await Scrypt.kdf("secret_password", { logN: 15, r: 8, p: 1 }) const password_hash = buf.toString("base64") - await manager.insert(User, { + const user = await manager.insert(User, { id: "admin_user", email: "admin@medusa.js", api_token: "test_token", @@ -15,4 +15,6 @@ module.exports = async (dataSource, data = {}) => { password_hash, ...data, }) + + return { user, password_hash } } diff --git a/integration-tests/helpers/create-admin-user.ts b/integration-tests/helpers/create-admin-user.ts index b105913b5b..bb871628ac 100644 --- a/integration-tests/helpers/create-admin-user.ts +++ b/integration-tests/helpers/create-admin-user.ts @@ -13,7 +13,7 @@ export const createAdminUser = async ( adminHeaders, container? ) => { - await adminSeeder(dbConnection) + const { password_hash } = await adminSeeder(dbConnection) const appContainer = container ?? getContainer()! const authModule: IAuthModuleService = appContainer.resolve( @@ -27,6 +27,9 @@ export const createAdminUser = async ( app_metadata: { user_id: "admin_user", }, + provider_metadata: { + password: password_hash, + }, }) const token = jwt.sign(authUser, "test") diff --git a/packages/auth/src/providers/google.ts b/packages/auth/src/providers/google.ts index a66d21e73c..159b65a000 100644 --- a/packages/auth/src/providers/google.ts +++ b/packages/auth/src/providers/google.ts @@ -1,8 +1,4 @@ -import { - AuthenticationInput, - AuthenticationResponse, - ModulesSdkTypes, -} from "@medusajs/types" +import { AuthenticationInput, AuthenticationResponse } from "@medusajs/types" import { AbstractAuthModuleProvider, MedusaError } from "@medusajs/utils" import { AuthUserService } from "@services" import jwt, { JwtPayload } from "jsonwebtoken" @@ -12,13 +8,13 @@ import url from "url" type InjectedDependencies = { authUserService: AuthUserService - authProviderService: ModulesSdkTypes.InternalModuleService } type ProviderConfig = { clientID: string clientSecret: string callbackURL: string + successRedirectUrl?: string } class GoogleProvider extends AbstractAuthModuleProvider { @@ -26,16 +22,14 @@ class GoogleProvider extends AbstractAuthModuleProvider { public static DISPLAY_NAME = "Google Authentication" protected readonly authUserService_: AuthUserService - protected readonly authProviderService_: ModulesSdkTypes.InternalModuleService - constructor({ authUserService, authProviderService }: InjectedDependencies) { + constructor({ authUserService }: InjectedDependencies) { super(arguments[0], { provider: GoogleProvider.PROVIDER, displayName: GoogleProvider.DISPLAY_NAME, }) this.authUserService_ = authUserService - this.authProviderService_ = authProviderService } async authenticate( @@ -112,7 +106,10 @@ class GoogleProvider extends AbstractAuthModuleProvider { } } - return { success: true, authUser } + return { + success: true, + authUser, + } } // abstractable @@ -130,7 +127,17 @@ class GoogleProvider extends AbstractAuthModuleProvider { try { const accessToken = await client.getToken(tokenParams) - return await this.verify_(accessToken.token.id_token) + const { authUser, success } = await this.verify_( + accessToken.token.id_token + ) + + const { successRedirectUrl } = this.getConfigFromScope() + + return { + success, + authUser, + successRedirectUrl, + } } catch (error) { return { success: false, error: error.message } } @@ -165,8 +172,6 @@ class GoogleProvider extends AbstractAuthModuleProvider { private async getProviderConfig( req: AuthenticationInput ): Promise { - await this.authProviderService_.retrieve(GoogleProvider.PROVIDER) - const config = this.getConfigFromScope() const callbackURL = config.callbackURL diff --git a/packages/auth/src/services/auth-user.ts b/packages/auth/src/services/auth-user.ts index a8e6fcc264..cd70a47ad0 100644 --- a/packages/auth/src/services/auth-user.ts +++ b/packages/auth/src/services/auth-user.ts @@ -14,6 +14,7 @@ import { import { AuthUser } from "@models" type InjectedDependencies = { + baseRepository: DAL.RepositoryService authUserRepository: DAL.RepositoryService } @@ -23,11 +24,13 @@ export default class AuthUserService< AuthUser ) { protected readonly authUserRepository_: RepositoryService + protected baseRepository_: DAL.RepositoryService constructor(container: InjectedDependencies) { // @ts-ignore super(...arguments) this.authUserRepository_ = container.authUserRepository + this.baseRepository_ = container.baseRepository } @InjectManager("authUserRepository_") @@ -36,7 +39,7 @@ export default class AuthUserService< provider: string, config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise { const queryConfig = ModulesSdkUtils.buildQuery( { entity_id: entityId, provider }, { ...config, take: 1 } @@ -53,6 +56,6 @@ export default class AuthUserService< ) } - return result + return await this.baseRepository_.serialize(result) } } diff --git a/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/callback/route.ts b/packages/medusa/src/api-v2/auth/[scope]/[auth_provider]/callback/route.ts similarity index 71% rename from packages/medusa/src/api-v2/auth/[scope]/[authProvider]/callback/route.ts rename to packages/medusa/src/api-v2/auth/[scope]/[auth_provider]/callback/route.ts index 3327e307c9..e1cade11a0 100644 --- a/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/callback/route.ts +++ b/packages/medusa/src/api-v2/auth/[scope]/[auth_provider]/callback/route.ts @@ -1,11 +1,11 @@ -import jwt from "jsonwebtoken" -import { MedusaError } from "@medusajs/utils" import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { AuthenticationInput, IAuthModuleService } from "@medusajs/types" +import { MedusaError } from "@medusajs/utils" +import jwt from "jsonwebtoken" import { MedusaRequest, MedusaResponse } from "../../../../../types/routing" export const GET = async (req: MedusaRequest, res: MedusaResponse) => { - const { scope, authProvider } = req.params + const { scope, auth_provider } = req.params const service: IAuthModuleService = req.scope.resolve( ModuleRegistrationName.AUTH @@ -20,18 +20,23 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { protocol: req.protocol, } as AuthenticationInput - const authResult = await service.validateCallback(authProvider, authData) + const authResult = await service.validateCallback(auth_provider, authData) - const { success, error, authUser, location } = authResult - if (location) { - res.redirect(location) - return - } + const { success, error, authUser, successRedirectUrl } = authResult if (success) { const { jwt_secret } = req.scope.resolve("configModule").projectConfig + const token = jwt.sign(authUser, jwt_secret) - return res.status(200).json({ token }) + + if (successRedirectUrl) { + const url = new URL(successRedirectUrl!) + url.searchParams.append("auth_token", token) + + return res.redirect(url.toString()) + } + + return res.json({ token }) } throw new MedusaError( diff --git a/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/route.ts b/packages/medusa/src/api-v2/auth/[scope]/[auth_provider]/route.ts similarity index 90% rename from packages/medusa/src/api-v2/auth/[scope]/[authProvider]/route.ts rename to packages/medusa/src/api-v2/auth/[scope]/[auth_provider]/route.ts index 5261b5494a..088f40f9fc 100644 --- a/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/route.ts +++ b/packages/medusa/src/api-v2/auth/[scope]/[auth_provider]/route.ts @@ -5,7 +5,7 @@ import jwt from "jsonwebtoken" import { MedusaRequest, MedusaResponse } from "../../../../types/routing" export const GET = async (req: MedusaRequest, res: MedusaResponse) => { - const { scope, authProvider } = req.params + const { scope, auth_provider } = req.params const service: IAuthModuleService = req.scope.resolve( ModuleRegistrationName.AUTH @@ -20,7 +20,7 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { protocol: req.protocol, } as AuthenticationInput - const authResult = await service.authenticate(authProvider, authData) + const authResult = await service.authenticate(auth_provider, authData) const { success, error, authUser, location } = authResult diff --git a/packages/medusa/src/api-v2/auth/middlewares.ts b/packages/medusa/src/api-v2/auth/middlewares.ts index ee8b4eb5a3..499a192c48 100644 --- a/packages/medusa/src/api-v2/auth/middlewares.ts +++ b/packages/medusa/src/api-v2/auth/middlewares.ts @@ -7,4 +7,14 @@ export const authRoutesMiddlewares: MiddlewareRoute[] = [ matcher: "/auth/session", middlewares: [authenticate(/.*/, "bearer")], }, + { + method: ["POST"], + matcher: "/auth/:scope/:auth_provider/callback", + middlewares: [], + }, + { + method: ["POST"], + matcher: "/auth/:scope/:auth_provider", + middlewares: [], + }, ] diff --git a/packages/types/src/auth/common/provider.ts b/packages/types/src/auth/common/provider.ts index 32d189c990..7dccb6db44 100644 --- a/packages/types/src/auth/common/provider.ts +++ b/packages/types/src/auth/common/provider.ts @@ -24,6 +24,11 @@ export type AuthenticationResponse = { * Redirect location. Location takes precedence over success. */ location?: string + + /** + * Redirect url for successful authentication. + */ + successRedirectUrl?: string } /**