From 72f0e149bb0fbe3242002a6c08ff9d1ec1f051e2 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Tue, 9 Jan 2024 09:07:21 +0100 Subject: [PATCH 01/53] init --- ...{provider-loaders.ts => providers.spec.ts} | 26 ++++++- .../providers/username-password.spec.ts | 75 +++++++++++++++++++ packages/authentication/package.json | 4 +- .../src/providers/username-password.ts | 38 +++++++++- .../src/services/authentication-module.ts | 39 +++++++++- packages/types/src/authentication/service.ts | 5 ++ yarn.lock | 1 + 7 files changed, 183 insertions(+), 5 deletions(-) rename packages/authentication/integration-tests/__tests__/services/module/{provider-loaders.ts => providers.spec.ts} (62%) create mode 100644 packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts diff --git a/packages/authentication/integration-tests/__tests__/services/module/provider-loaders.ts b/packages/authentication/integration-tests/__tests__/services/module/providers.spec.ts similarity index 62% rename from packages/authentication/integration-tests/__tests__/services/module/provider-loaders.ts rename to packages/authentication/integration-tests/__tests__/services/module/providers.spec.ts index 36e513f95fb86..c9ae78c081e5b 100644 --- a/packages/authentication/integration-tests/__tests__/services/module/provider-loaders.ts +++ b/packages/authentication/integration-tests/__tests__/services/module/providers.spec.ts @@ -5,6 +5,7 @@ import { initialize } from "../../../../src" import { DB_URL } from "@medusajs/pricing/integration-tests/utils" import { MedusaModule } from "@medusajs/modules-sdk" import { IAuthenticationModuleService } from "@medusajs/types" +import { createAuthProviders } from "../../../__fixtures__/auth-provider" jest.setTimeout(30000) @@ -30,7 +31,7 @@ describe("AuthenticationModuleService - AuthProvider", () => { }) describe("listAuthProviders", () => { - it("should list default AuthProviders", async () => { + it("should list default AuthProviders registered by loaders", async () => { const authProviders = await service.listAuthProviders() const serialized = JSON.parse(JSON.stringify(authProviders)) @@ -42,4 +43,27 @@ describe("AuthenticationModuleService - AuthProvider", () => { ]) }) }) + + describe("authenticate", () => { + it("authenticate validates that a provider is registered in container", async () => { + await createAuthProviders(testManager, [ + { + provider: "notRegistered", + name: "test", + }, + ]) + + let error + + try { + await service.authenticate("notRegistered", {}) + } catch (err) { + error = err + } + + expect(error.message).toEqual( + "AuthenticationProvider with for provider: notRegistered wasn't registered in the module. Have you configured your options correctly?" + ) + }) + }) }) diff --git a/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts b/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts new file mode 100644 index 0000000000000..a1bbbbf56e1f2 --- /dev/null +++ b/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts @@ -0,0 +1,75 @@ +import { SqlEntityManager } from "@mikro-orm/postgresql" +import Scrypt from "scrypt-kdf" + +import { MikroOrmWrapper } from "../../../utils" +import { initialize } from "../../../../src" +import { DB_URL } from "@medusajs/pricing/integration-tests/utils" +import { MedusaModule } from "@medusajs/modules-sdk" +import { IAuthenticationModuleService } from "@medusajs/types" +import { createAuthUsers } from "../../../__fixtures__/auth-user" +import { createAuthProviders } from "../../../__fixtures__/auth-provider" + +jest.setTimeout(30000) +const seedDefaultData = async (testManager) => { + await createAuthProviders(testManager) + await createAuthUsers(testManager) +} + +describe("AuthenticationModuleService - AuthProvider", () => { + let service: IAuthenticationModuleService + let testManager: SqlEntityManager + + beforeEach(async () => { + await MikroOrmWrapper.setupDatabase() + testManager = MikroOrmWrapper.forkManager() + + service = await initialize({ + database: { + clientUrl: DB_URL, + schema: process.env.MEDUSA_PRICING_DB_SCHEMA, + }, + }) + }) + + afterEach(async () => { + await MikroOrmWrapper.clearDatabase() + MedusaModule.clearInstances() + }) + + describe("authenticate", () => { + it("authenticate validates that a provider is registered in container", async () => { + const password = "supersecret" + const email = "test@test.com" + const passwordHash = ( + await Scrypt.kdf(password, { logN: 15, r: 8, p: 1 }) + ).toString("base64") + + await seedDefaultData(testManager) + await createAuthUsers(testManager, [ + // Add authenticated user + { + provider: "usernamePassword", + provider_metadata: { + email, + password_hash: passwordHash, + }, + }, + ]) + + const res = await service.authenticate("usernamePassword", { + email: "test@test.com", + password: password, + }) + + expect(res).toEqual({ + success: true, + authUser: expect.objectContaining({ + provider_metadata: { + email, + password_hash: passwordHash, + }, + }), + }) + }) + }) +}) diff --git a/packages/authentication/package.json b/packages/authentication/package.json index 05fb16fbbf1cd..29cafb9c0f68e 100644 --- a/packages/authentication/package.json +++ b/packages/authentication/package.json @@ -30,6 +30,7 @@ "build": "rimraf dist && tsc --build && tsc-alias -p tsconfig.json", "test": "jest --runInBand --bail --forceExit -- src/**/__tests__/**/*.ts", "test:integration": "jest --runInBand --forceExit -- integration-tests/**/__tests__/**/*.ts", + "test:integration:single": "jest --runInBand --forceExit ", "migration:generate": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:generate", "migration:initial": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create --initial", "migration:create": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create", @@ -56,6 +57,7 @@ "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.0", "dotenv": "^16.1.4", - "knex": "2.4.2" + "knex": "2.4.2", + "scrypt-kdf": "^2.0.1" } } diff --git a/packages/authentication/src/providers/username-password.ts b/packages/authentication/src/providers/username-password.ts index 1b3ca03a17806..7e48a70c9c371 100644 --- a/packages/authentication/src/providers/username-password.ts +++ b/packages/authentication/src/providers/username-password.ts @@ -1,3 +1,5 @@ +import Scrypt from "scrypt-kdf" + import { AuthUserService } from "@services" import { AbstractAuthenticationModuleProvider } from "@medusajs/types" @@ -14,7 +16,41 @@ class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { } async authenticate(userData: Record) { - return {} + const { email, password } = userData + + if (typeof password !== "string") { + return { + success: false, + error: "Password should be a string", + } + } + if (typeof email !== "string") { + return { + success: false, + error: "Email should be a string", + } + } + + const [authUser] = await this.authUserSerivce_.list({ + provider_metadata: { + email, + }, + }) + + const password_hash = authUser.provider_metadata?.password_hash + + if (password_hash && typeof password_hash === "string") { + const buf = Buffer.from(password_hash, "base64") + + const success = await Scrypt.verify(buf, password) + + return { success, authUser } + } + + return { + success: false, + error: "Invalid email or password", + } } } diff --git a/packages/authentication/src/services/authentication-module.ts b/packages/authentication/src/services/authentication-module.ts index d47c094b3e1b1..10ca1680abb60 100644 --- a/packages/authentication/src/services/authentication-module.ts +++ b/packages/authentication/src/services/authentication-module.ts @@ -1,9 +1,11 @@ import { + AbstractAuthenticationModuleProvider, AuthenticationTypes, Context, DAL, FindConfig, InternalModuleDeclaration, + MedusaContainer, ModuleJoinerConfig, } from "@medusajs/types" @@ -15,6 +17,7 @@ import { InjectManager, InjectTransactionManager, MedusaContext, + MedusaError, } from "@medusajs/utils" import { AuthProviderDTO, @@ -37,6 +40,11 @@ export default class AuthenticationModuleService< TAuthProvider extends AuthProvider = AuthProvider > implements AuthenticationTypes.IAuthenticationModuleService { + __joinerConfig(): ModuleJoinerConfig { + return joinerConfig + } + + protected __container__: MedusaContainer protected baseRepository_: DAL.RepositoryService protected authUserService_: AuthUserService @@ -50,6 +58,7 @@ export default class AuthenticationModuleService< }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { + this.__container__ = arguments[0] this.baseRepository_ = baseRepository this.authUserService_ = authUserService this.authProviderService_ = authProviderService @@ -336,7 +345,33 @@ export default class AuthenticationModuleService< await this.authUserService_.delete(ids, sharedContext) } - __joinerConfig(): ModuleJoinerConfig { - return joinerConfig + protected getRegisteredAuthenticationProvider( + provider: string + ): AbstractAuthenticationModuleProvider { + let containerProvider: AbstractAuthenticationModuleProvider + try { + containerProvider = this.__container__[`provider_${provider}`] + } catch (error) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `AuthenticationProvider with for provider: ${provider} wasn't registered in the module. Have you configured your options correctly?` + ) + } + + return containerProvider + } + + @InjectTransactionManager("baseRepository_") + async authenticate( + provider: string, + authenticationData: Record, + @MedusaContext() sharedContext: Context = {} + ): Promise> { + await this.retrieveAuthProvider(provider, {}) + + const registeredProvider = + this.getRegisteredAuthenticationProvider(provider) + + return await registeredProvider.authenticate(authenticationData) } } diff --git a/packages/types/src/authentication/service.ts b/packages/types/src/authentication/service.ts index 78df1eabca25b..d0e9e03de1a40 100644 --- a/packages/types/src/authentication/service.ts +++ b/packages/types/src/authentication/service.ts @@ -13,6 +13,11 @@ import { FindConfig } from "../common" import { Context } from "../shared-context" export interface IAuthenticationModuleService extends IModuleService { + authenticate( + provider: string, + providerData: Record + ): Promise> + retrieveAuthProvider( provider: string, config?: FindConfig, diff --git a/yarn.lock b/yarn.lock index ee39d59dc07a7..dcfec06735e9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7857,6 +7857,7 @@ __metadata: knex: 2.4.2 medusa-test-utils: ^1.1.40 rimraf: ^3.0.2 + scrypt-kdf: ^2.0.1 ts-jest: ^29.1.1 ts-node: ^10.9.1 tsc-alias: ^1.8.6 From f8b70898a18c7d94520e936e411a92222591e0c6 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Tue, 9 Jan 2024 11:23:36 +0100 Subject: [PATCH 02/53] fetch providers using updated string --- packages/authentication/src/services/authentication-module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/authentication/src/services/authentication-module.ts b/packages/authentication/src/services/authentication-module.ts index 10ca1680abb60..0c6ea9e1ed248 100644 --- a/packages/authentication/src/services/authentication-module.ts +++ b/packages/authentication/src/services/authentication-module.ts @@ -350,7 +350,7 @@ export default class AuthenticationModuleService< ): AbstractAuthenticationModuleProvider { let containerProvider: AbstractAuthenticationModuleProvider try { - containerProvider = this.__container__[`provider_${provider}`] + containerProvider = this.__container__[`auth_provider_${provider}`] } catch (error) { throw new MedusaError( MedusaError.Types.NOT_FOUND, From 5adca1cdacfa1f899be5d1b61606875f4860dd65 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Tue, 9 Jan 2024 09:46:40 +0100 Subject: [PATCH 03/53] update loaders --- packages/authentication/src/loaders/providers.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/authentication/src/loaders/providers.ts b/packages/authentication/src/loaders/providers.ts index d21a31db0bcfb..64d2019a66132 100644 --- a/packages/authentication/src/loaders/providers.ts +++ b/packages/authentication/src/loaders/providers.ts @@ -17,8 +17,9 @@ export default async ({ const providersToLoad = Object.values(defaultProviders) - const authProviderService: AuthProviderService = - container.cradle["authProviderService"] + const authProviderService: AuthProviderService = container.resolve( + "authProviderService" + ) const providers = await authProviderService.list({ provider: providersToLoad.map((p) => p.PROVIDER), @@ -29,10 +30,8 @@ export default async ({ const providersToCreate: ServiceTypes.CreateAuthProviderDTO[] = [] for (const provider of providersToLoad) { - container.registerAdd("providers", asClass(provider).singleton()) - container.register({ - [`provider_${provider.PROVIDER}`]: asClass(provider).singleton(), + [`auth_provider_${provider.PROVIDER}`]: asClass(provider).singleton(), }) if (loadedProviders.has(provider.PROVIDER)) { From 276f3ebc28c5ff96cd8474ef04abae101dc969e1 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 10 Jan 2024 13:14:27 +0100 Subject: [PATCH 04/53] add more tests --- .../providers/username-password.spec.ts | 58 +++++++++++++++++++ .../src/providers/username-password.ts | 5 +- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts b/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts index a1bbbbf56e1f2..ef0ab208f32e7 100644 --- a/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts +++ b/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts @@ -71,5 +71,63 @@ describe("AuthenticationModuleService - AuthProvider", () => { }), }) }) + + it("fails when no password is given", async () => { + const email = "test@test.com" + + await seedDefaultData(testManager) + + const res = await service.authenticate("usernamePassword", { + email: "test@test.com", + }) + + expect(res).toEqual({ + success: false, + error: "Password should be a string", + }) + }) + + it("fails when no email is given", async () => { + await seedDefaultData(testManager) + + const res = await service.authenticate("usernamePassword", { + password: "supersecret", + }) + + expect(res).toEqual({ + success: false, + error: "Email should be a string", + }) + }) + + it("fails with an invalid password", async () => { + const password = "supersecret" + const email = "test@test.com" + const passwordHash = ( + await Scrypt.kdf(password, { logN: 15, r: 8, p: 1 }) + ).toString("base64") + + await seedDefaultData(testManager) + await createAuthUsers(testManager, [ + // Add authenticated user + { + provider: "usernamePassword", + provider_metadata: { + email, + password_hash: passwordHash, + }, + }, + ]) + + const res = await service.authenticate("usernamePassword", { + email: "test@test.com", + password: "password", + }) + + expect(res).toEqual({ + success: false, + error: "Invalid email or password", + }) + }) }) }) diff --git a/packages/authentication/src/providers/username-password.ts b/packages/authentication/src/providers/username-password.ts index 7e48a70c9c371..7c25eeb0007d2 100644 --- a/packages/authentication/src/providers/username-password.ts +++ b/packages/authentication/src/providers/username-password.ts @@ -24,6 +24,7 @@ class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { error: "Password should be a string", } } + if (typeof email !== "string") { return { success: false, @@ -44,7 +45,9 @@ class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { const success = await Scrypt.verify(buf, password) - return { success, authUser } + if (success) { + return { success, authUser } + } } return { From 01578e71f91f0428b672fd4898058b453c0866b8 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Thu, 11 Jan 2024 09:14:19 +0100 Subject: [PATCH 05/53] add authenticationresponse type --- packages/types/src/authentication/provider.ts | 9 ++++++++- packages/types/src/authentication/service.ts | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/types/src/authentication/provider.ts b/packages/types/src/authentication/provider.ts index db950c2077f5f..c4b07e73c9dfd 100644 --- a/packages/types/src/authentication/provider.ts +++ b/packages/types/src/authentication/provider.ts @@ -1,8 +1,15 @@ +import { AuthUserDTO } from "./common" + export abstract class AbstractAuthenticationModuleProvider { public static PROVIDER: string public static DISPLAY_NAME: string abstract authenticate( data: Record - ): Promise> + ): Promise +} + +export type AuthenticationResponse = { + success: boolean + authUser: AuthUserDTO } diff --git a/packages/types/src/authentication/service.ts b/packages/types/src/authentication/service.ts index d0e9e03de1a40..64d63fb25e574 100644 --- a/packages/types/src/authentication/service.ts +++ b/packages/types/src/authentication/service.ts @@ -11,12 +11,13 @@ import { } from "./common" import { FindConfig } from "../common" import { Context } from "../shared-context" +import { AuthenticationResponse } from "./provider" export interface IAuthenticationModuleService extends IModuleService { authenticate( provider: string, providerData: Record - ): Promise> + ): Promise retrieveAuthProvider( provider: string, From 757039de769cc6714d92ee51ffe0034425fcdf64 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Thu, 11 Jan 2024 11:06:56 +0100 Subject: [PATCH 06/53] update types for authentication method --- .../authentication/src/providers/username-password.ts | 11 ++++++++--- .../src/services/authentication-module.ts | 3 ++- packages/types/src/authentication/provider.ts | 3 ++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/authentication/src/providers/username-password.ts b/packages/authentication/src/providers/username-password.ts index 7c25eeb0007d2..1ba4a458f4b13 100644 --- a/packages/authentication/src/providers/username-password.ts +++ b/packages/authentication/src/providers/username-password.ts @@ -1,7 +1,10 @@ import Scrypt from "scrypt-kdf" import { AuthUserService } from "@services" -import { AbstractAuthenticationModuleProvider } from "@medusajs/types" +import { + AbstractAuthenticationModuleProvider, + AuthenticationResponse, +} from "@medusajs/types" class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { public static PROVIDER = "usernamePassword" @@ -15,7 +18,9 @@ class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { this.authUserSerivce_ = AuthUserService } - async authenticate(userData: Record) { + async authenticate( + userData: Record + ): Promise { const { email, password } = userData if (typeof password !== "string") { @@ -46,7 +51,7 @@ class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { const success = await Scrypt.verify(buf, password) if (success) { - return { success, authUser } + return { success, authUser: JSON.parse(JSON.stringify(authUser)) } } } diff --git a/packages/authentication/src/services/authentication-module.ts b/packages/authentication/src/services/authentication-module.ts index 0c6ea9e1ed248..aa55f8fb33de1 100644 --- a/packages/authentication/src/services/authentication-module.ts +++ b/packages/authentication/src/services/authentication-module.ts @@ -1,5 +1,6 @@ import { AbstractAuthenticationModuleProvider, + AuthenticationResponse, AuthenticationTypes, Context, DAL, @@ -366,7 +367,7 @@ export default class AuthenticationModuleService< provider: string, authenticationData: Record, @MedusaContext() sharedContext: Context = {} - ): Promise> { + ): Promise { await this.retrieveAuthProvider(provider, {}) const registeredProvider = diff --git a/packages/types/src/authentication/provider.ts b/packages/types/src/authentication/provider.ts index c4b07e73c9dfd..7c2e0bafd3e76 100644 --- a/packages/types/src/authentication/provider.ts +++ b/packages/types/src/authentication/provider.ts @@ -11,5 +11,6 @@ export abstract class AbstractAuthenticationModuleProvider { export type AuthenticationResponse = { success: boolean - authUser: AuthUserDTO + authUser?: AuthUserDTO + error?: string } From 2adb779803ed66b218e096a20faa4eeeeaefaeb3 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Mon, 15 Jan 2024 15:26:41 +0100 Subject: [PATCH 07/53] add entity_id and update provider --- .../providers/username-password.spec.ts | 2 +- .../authentication/src/initialize/index.ts | 5 ++- .../authentication/src/loaders/providers.ts | 20 ++++++--- .../.snapshot-medusa-authentication.json | 19 +++++++++ .../src/migrations/Migration20240115092929.ts | 15 +++++++ .../authentication/src/models/auth-user.ts | 9 +++- .../src/providers/username-password.ts | 19 ++++----- .../authentication/src/services/auth-user.ts | 41 +++++++++++++++++-- .../src/services/authentication-module.ts | 14 ++++--- 9 files changed, 116 insertions(+), 28 deletions(-) create mode 100644 packages/authentication/src/migrations/Migration20240115092929.ts diff --git a/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts b/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts index ef0ab208f32e7..78a3665ec3b9d 100644 --- a/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts +++ b/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts @@ -51,7 +51,7 @@ describe("AuthenticationModuleService - AuthProvider", () => { provider: "usernamePassword", provider_metadata: { email, - password_hash: passwordHash, + password: passwordHash, }, }, ]) diff --git a/packages/authentication/src/initialize/index.ts b/packages/authentication/src/initialize/index.ts index cfd284f1e965d..2328c27688c6c 100644 --- a/packages/authentication/src/initialize/index.ts +++ b/packages/authentication/src/initialize/index.ts @@ -1,13 +1,14 @@ import { ExternalModuleDeclaration, InternalModuleDeclaration, - MedusaModule, MODULE_PACKAGE_NAMES, + MedusaModule, Modules, } from "@medusajs/modules-sdk" import { IAuthenticationModuleService, ModulesSdkTypes } from "@medusajs/types" -import { moduleDefinition } from "../module-definition" + import { InitializeModuleInjectableDependencies } from "../types" +import { moduleDefinition } from "../module-definition" export const initialize = async ( options?: diff --git a/packages/authentication/src/loaders/providers.ts b/packages/authentication/src/loaders/providers.ts index 64d2019a66132..c9ff2ee18fa52 100644 --- a/packages/authentication/src/loaders/providers.ts +++ b/packages/authentication/src/loaders/providers.ts @@ -1,8 +1,10 @@ -import { LoaderOptions, ModulesSdkTypes } from "@medusajs/types" -import { asClass } from "awilix" import * as defaultProviders from "@providers" + +import { LoaderOptions, ModulesSdkTypes } from "@medusajs/types" + import { AuthProviderService } from "@services" import { ServiceTypes } from "@types" +import { asClass } from "awilix" export default async ({ container, @@ -20,10 +22,18 @@ export default async ({ const authProviderService: AuthProviderService = container.resolve( "authProviderService" ) + let providers - const providers = await authProviderService.list({ - provider: providersToLoad.map((p) => p.PROVIDER), - }) + try { + providers = await authProviderService.list({ + provider: providersToLoad.map((p) => p.PROVIDER), + }) + } catch (error) { + if (error.name === "TableNotFoundException") { + // we are running loaders in migrations (or fail at a later point) + return + } + } const loadedProviders = new Map(providers.map((p) => [p.provider, p])) diff --git a/packages/authentication/src/migrations/.snapshot-medusa-authentication.json b/packages/authentication/src/migrations/.snapshot-medusa-authentication.json index 0d3f1eaba19f5..f7ebcf17c1ad9 100644 --- a/packages/authentication/src/migrations/.snapshot-medusa-authentication.json +++ b/packages/authentication/src/migrations/.snapshot-medusa-authentication.json @@ -77,6 +77,15 @@ "nullable": false, "mappedType": "text" }, + "entity_id": { + "name": "entity_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, "provider_id": { "name": "provider_id", "type": "text", @@ -117,6 +126,16 @@ "name": "auth_user", "schema": "public", "indexes": [ + { + "keyName": "IDX_auth_user_provider_entity_id", + "columnNames": [ + "provider_id", + "entity_id" + ], + "composite": true, + "primary": false, + "unique": true + }, { "keyName": "auth_user_pkey", "columnNames": [ diff --git a/packages/authentication/src/migrations/Migration20240115092929.ts b/packages/authentication/src/migrations/Migration20240115092929.ts new file mode 100644 index 0000000000000..d06392934456e --- /dev/null +++ b/packages/authentication/src/migrations/Migration20240115092929.ts @@ -0,0 +1,15 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20240115092929 extends Migration { + + async up(): Promise { + this.addSql('alter table "auth_user" add column "entity_id" text not null;'); + this.addSql('alter table "auth_user" add constraint "IDX_auth_user_provider_entity_id" unique ("provider_id", "entity_id");'); + } + + async down(): Promise { + this.addSql('alter table "auth_user" drop constraint "IDX_auth_user_provider_entity_id";'); + this.addSql('alter table "auth_user" drop column "entity_id";'); + } + +} diff --git a/packages/authentication/src/models/auth-user.ts b/packages/authentication/src/models/auth-user.ts index e90cb10130c03..0c10053175fae 100644 --- a/packages/authentication/src/models/auth-user.ts +++ b/packages/authentication/src/models/auth-user.ts @@ -1,25 +1,32 @@ -import { generateEntityId } from "@medusajs/utils" import { BeforeCreate, Cascade, Entity, + Index, ManyToOne, OnInit, OptionalProps, PrimaryKey, Property, + Unique, } from "@mikro-orm/core" + import AuthProvider from "./auth-provider" +import { generateEntityId } from "@medusajs/utils" type OptionalFields = "provider_metadata" | "app_metadata" | "user_metadata" @Entity() +@Unique({ properties: ["provider","entity_id" ], name: "IDX_auth_user_provider_entity_id" }) export default class AuthUser { [OptionalProps]: OptionalFields @PrimaryKey({ columnType: "text" }) id!: string + @Property({ columnType: "text" }) + entity_id: string + @ManyToOne(() => AuthProvider, { joinColumn: "provider", fieldName: "provider_id", diff --git a/packages/authentication/src/providers/username-password.ts b/packages/authentication/src/providers/username-password.ts index 1ba4a458f4b13..fd221dd75d124 100644 --- a/packages/authentication/src/providers/username-password.ts +++ b/packages/authentication/src/providers/username-password.ts @@ -1,11 +1,11 @@ -import Scrypt from "scrypt-kdf" - -import { AuthUserService } from "@services" import { AbstractAuthenticationModuleProvider, AuthenticationResponse, } from "@medusajs/types" +import { AuthUserService } from "@services" +import Scrypt from "scrypt-kdf" + class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { public static PROVIDER = "usernamePassword" public static DISPLAY_NAME = "Username/Password Authentication" @@ -19,9 +19,9 @@ class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { } async authenticate( - userData: Record + userData: Record ): Promise { - const { email, password } = userData + const { email, password } = userData.body if (typeof password !== "string") { return { @@ -37,11 +37,10 @@ class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { } } - const [authUser] = await this.authUserSerivce_.list({ - provider_metadata: { - email, - }, - }) + const authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( + email, + UsernamePasswordProvider.PROVIDER + ) const password_hash = authUser.provider_metadata?.password_hash diff --git a/packages/authentication/src/services/auth-user.ts b/packages/authentication/src/services/auth-user.ts index 352d818a2a84d..c85dda861e782 100644 --- a/packages/authentication/src/services/auth-user.ts +++ b/packages/authentication/src/services/auth-user.ts @@ -1,8 +1,12 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" +import { AuthenticationTypes, Context, DAL, FindConfig } from "@medusajs/types" +import { + InjectManager, + MedusaContext, + MedusaError, + ModulesSdkUtils, +} from "@medusajs/utils" import { AuthUser } from "@models" - -import { ServiceTypes } from "@types" +import { ServiceTypes, RepositoryTypes } from "@types" type InjectedDependencies = { authUserRepository: DAL.RepositoryService @@ -16,8 +20,37 @@ export default class AuthUserService< create: ServiceTypes.CreateAuthUserDTO } >(AuthUser) { + protected readonly authUserRepository_: RepositoryTypes.IAuthUserRepository constructor(container: InjectedDependencies) { // @ts-ignore super(...arguments) } + + @InjectManager("authUserRepository_") + async retrieveByProviderAndEntityId< + TEntityMethod = AuthenticationTypes.AuthUserDTO + >( + entityId: string, + provider: string, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const queryConfig = ModulesSdkUtils.buildQuery( + { entity_id: entityId, provider }, + { ...config, take: 1 } + ) + const [result] = await this.authUserRepository_.find( + queryConfig, + sharedContext + ) + + if (!result) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `AuthUser with entity_id: "${entityId}" and provider: "${provider}" not found` + ) + } + + return result + } } diff --git a/packages/authentication/src/services/authentication-module.ts b/packages/authentication/src/services/authentication-module.ts index aa55f8fb33de1..544412eb619c3 100644 --- a/packages/authentication/src/services/authentication-module.ts +++ b/packages/authentication/src/services/authentication-module.ts @@ -367,11 +367,15 @@ export default class AuthenticationModuleService< provider: string, authenticationData: Record, @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.retrieveAuthProvider(provider, {}) - - const registeredProvider = - this.getRegisteredAuthenticationProvider(provider) + ): Promise { + let registeredProvider + try { + await this.retrieveAuthProvider(provider, {}) + + registeredProvider = this.getRegisteredAuthenticationProvider(provider) + } catch (error) { + return { success: false, error: error.message } + } return await registeredProvider.authenticate(authenticationData) } From 6db3a46b2be856ae079f5c70cf75708d15419fec Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 08:33:33 +0100 Subject: [PATCH 08/53] update pr with return type --- .../__fixtures__/auth-user/index.ts | 3 +++ .../services/auth-user/index.spec.ts | 1 + .../services/module/auth-user.spec.ts | 1 + .../services/module/providers.spec.ts | 11 +++------ .../providers/username-password.spec.ts | 23 +++++++++++-------- packages/authentication/package.json | 2 +- .../src/providers/username-password.ts | 4 +++- .../src/services/authentication-module.ts | 9 ++++++-- .../src/types/repositories/auth-user.ts | 1 + .../src/types/services/auth-user.ts | 2 ++ .../src/authentication/common/auth-user.ts | 2 ++ 11 files changed, 37 insertions(+), 22 deletions(-) diff --git a/packages/authentication/integration-tests/__fixtures__/auth-user/index.ts b/packages/authentication/integration-tests/__fixtures__/auth-user/index.ts index 7921cbdf906f3..46e745ffdc18b 100644 --- a/packages/authentication/integration-tests/__fixtures__/auth-user/index.ts +++ b/packages/authentication/integration-tests/__fixtures__/auth-user/index.ts @@ -6,13 +6,16 @@ export async function createAuthUsers( userData: any[] = [ { id: "test-id", + entity_id: "test-id", provider: "manual", }, { id: "test-id-1", + entity_id: "test-id-1", provider: "manual", }, { + entity_id: "test-id-2", provider: "store", }, ] diff --git a/packages/authentication/integration-tests/__tests__/services/auth-user/index.spec.ts b/packages/authentication/integration-tests/__tests__/services/auth-user/index.spec.ts index eaaf4324ef83c..07f7aa2426d9e 100644 --- a/packages/authentication/integration-tests/__tests__/services/auth-user/index.spec.ts +++ b/packages/authentication/integration-tests/__tests__/services/auth-user/index.spec.ts @@ -229,6 +229,7 @@ describe("AuthUser Service", () => { { id: "test", provider_id: "manual", + entity_id: "test" }, ]) diff --git a/packages/authentication/integration-tests/__tests__/services/module/auth-user.spec.ts b/packages/authentication/integration-tests/__tests__/services/module/auth-user.spec.ts index 14dc31ef6690b..677fd8e35fc0a 100644 --- a/packages/authentication/integration-tests/__tests__/services/module/auth-user.spec.ts +++ b/packages/authentication/integration-tests/__tests__/services/module/auth-user.spec.ts @@ -237,6 +237,7 @@ describe("AuthenticationModuleService - AuthUser", () => { { id: "test", provider_id: "manual", + entity_id: "test" }, ]) diff --git a/packages/authentication/integration-tests/__tests__/services/module/providers.spec.ts b/packages/authentication/integration-tests/__tests__/services/module/providers.spec.ts index c9ae78c081e5b..607e6d0801c9d 100644 --- a/packages/authentication/integration-tests/__tests__/services/module/providers.spec.ts +++ b/packages/authentication/integration-tests/__tests__/services/module/providers.spec.ts @@ -53,15 +53,10 @@ describe("AuthenticationModuleService - AuthProvider", () => { }, ]) - let error + const { success, error } = await service.authenticate("notRegistered", {}) - try { - await service.authenticate("notRegistered", {}) - } catch (err) { - error = err - } - - expect(error.message).toEqual( + expect(success).toBe(false) + expect(error).toEqual( "AuthenticationProvider with for provider: notRegistered wasn't registered in the module. Have you configured your options correctly?" ) }) diff --git a/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts b/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts index 78a3665ec3b9d..93621858baab4 100644 --- a/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts +++ b/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts @@ -49,24 +49,25 @@ describe("AuthenticationModuleService - AuthProvider", () => { // Add authenticated user { provider: "usernamePassword", + entity_id: email, provider_metadata: { - email, password: passwordHash, }, }, ]) const res = await service.authenticate("usernamePassword", { - email: "test@test.com", - password: password, + body: { + email: "test@test.com", + password: password, + }, }) expect(res).toEqual({ success: true, authUser: expect.objectContaining({ + entity_id: email, provider_metadata: { - email, - password_hash: passwordHash, }, }), }) @@ -78,7 +79,7 @@ describe("AuthenticationModuleService - AuthProvider", () => { await seedDefaultData(testManager) const res = await service.authenticate("usernamePassword", { - email: "test@test.com", + body: { email: "test@test.com" }, }) expect(res).toEqual({ @@ -91,7 +92,7 @@ describe("AuthenticationModuleService - AuthProvider", () => { await seedDefaultData(testManager) const res = await service.authenticate("usernamePassword", { - password: "supersecret", + body: { password: "supersecret" }, }) expect(res).toEqual({ @@ -112,16 +113,18 @@ describe("AuthenticationModuleService - AuthProvider", () => { // Add authenticated user { provider: "usernamePassword", + entity_id: email, provider_metadata: { - email, password_hash: passwordHash, }, }, ]) const res = await service.authenticate("usernamePassword", { - email: "test@test.com", - password: "password", + body: { + email: "test@test.com", + password: "password", + }, }) expect(res).toEqual({ diff --git a/packages/authentication/package.json b/packages/authentication/package.json index 29cafb9c0f68e..f128ae7bb9ea6 100644 --- a/packages/authentication/package.json +++ b/packages/authentication/package.json @@ -29,7 +29,7 @@ "prepublishOnly": "cross-env NODE_ENV=production tsc --build && tsc-alias -p tsconfig.json", "build": "rimraf dist && tsc --build && tsc-alias -p tsconfig.json", "test": "jest --runInBand --bail --forceExit -- src/**/__tests__/**/*.ts", - "test:integration": "jest --runInBand --forceExit -- integration-tests/**/__tests__/**/*.ts", + "test:integration": "jest --runInBand --forceExit -- ", "test:integration:single": "jest --runInBand --forceExit ", "migration:generate": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:generate", "migration:initial": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create --initial", diff --git a/packages/authentication/src/providers/username-password.ts b/packages/authentication/src/providers/username-password.ts index fd221dd75d124..b77c716f1174b 100644 --- a/packages/authentication/src/providers/username-password.ts +++ b/packages/authentication/src/providers/username-password.ts @@ -42,7 +42,7 @@ class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { UsernamePasswordProvider.PROVIDER ) - const password_hash = authUser.provider_metadata?.password_hash + const password_hash = authUser.provider_metadata?.password if (password_hash && typeof password_hash === "string") { const buf = Buffer.from(password_hash, "base64") @@ -50,6 +50,8 @@ class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { const success = await Scrypt.verify(buf, password) if (success) { + delete authUser.provider_metadata!.password + return { success, authUser: JSON.parse(JSON.stringify(authUser)) } } } diff --git a/packages/authentication/src/services/authentication-module.ts b/packages/authentication/src/services/authentication-module.ts index 544412eb619c3..86ed10be7b617 100644 --- a/packages/authentication/src/services/authentication-module.ts +++ b/packages/authentication/src/services/authentication-module.ts @@ -351,8 +351,10 @@ export default class AuthenticationModuleService< ): AbstractAuthenticationModuleProvider { let containerProvider: AbstractAuthenticationModuleProvider try { + console.log("containerProvider") containerProvider = this.__container__[`auth_provider_${provider}`] } catch (error) { + console.log(error) throw new MedusaError( MedusaError.Types.NOT_FOUND, `AuthenticationProvider with for provider: ${provider} wasn't registered in the module. Have you configured your options correctly?` @@ -369,14 +371,17 @@ export default class AuthenticationModuleService< @MedusaContext() sharedContext: Context = {} ): Promise { let registeredProvider + console.log("hello") try { await this.retrieveAuthProvider(provider, {}) + console.log(registeredProvider) registeredProvider = this.getRegisteredAuthenticationProvider(provider) + + return await registeredProvider.authenticate(authenticationData) } catch (error) { + console.log(JSON.stringify(error, null, 2)) return { success: false, error: error.message } } - - return await registeredProvider.authenticate(authenticationData) } } diff --git a/packages/authentication/src/types/repositories/auth-user.ts b/packages/authentication/src/types/repositories/auth-user.ts index 33315b20e1600..541f465838772 100644 --- a/packages/authentication/src/types/repositories/auth-user.ts +++ b/packages/authentication/src/types/repositories/auth-user.ts @@ -2,6 +2,7 @@ import { AuthUser } from "@models" export type CreateAuthUserDTO = { provider_id: string + entity_id: string provider_metadata?: Record user_metadata?: Record app_metadata?: Record diff --git a/packages/authentication/src/types/services/auth-user.ts b/packages/authentication/src/types/services/auth-user.ts index 18c8899303ee6..c059e980f8b9c 100644 --- a/packages/authentication/src/types/services/auth-user.ts +++ b/packages/authentication/src/types/services/auth-user.ts @@ -3,6 +3,7 @@ import { AuthProviderDTO } from "./auth-provider" export type AuthUserDTO = { id: string provider_id: string + entity_id: string provider: AuthProviderDTO provider_metadata?: Record user_metadata: Record @@ -10,6 +11,7 @@ export type AuthUserDTO = { } export type CreateAuthUserDTO = { + entity_id: string provider_id: string provider_metadata?: Record user_metadata?: Record diff --git a/packages/types/src/authentication/common/auth-user.ts b/packages/types/src/authentication/common/auth-user.ts index ae5b03b0fe253..11357a10ebe8a 100644 --- a/packages/types/src/authentication/common/auth-user.ts +++ b/packages/types/src/authentication/common/auth-user.ts @@ -4,6 +4,7 @@ import { AuthProviderDTO } from "./auth-provider" export type AuthUserDTO = { id: string provider_id: string + entity_id: string provider: AuthProviderDTO provider_metadata?: Record user_metadata: Record @@ -12,6 +13,7 @@ export type AuthUserDTO = { export type CreateAuthUserDTO = { provider_id: string + entity_id: string provider_metadata?: Record user_metadata?: Record app_metadata?: Record From 0bf52305272a3380a033a5f8b316a0cffcb1eff7 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 10:26:27 +0100 Subject: [PATCH 09/53] create loaders onApplicationStart --- .../services/module/providers.spec.ts | 4 ++ .../authentication/src/loaders/providers.ts | 45 +++++-------------- .../src/services/authentication-module.ts | 45 ++++++++++++++----- packages/types/src/authentication/provider.ts | 16 +++++++ 4 files changed, 67 insertions(+), 43 deletions(-) diff --git a/packages/authentication/integration-tests/__tests__/services/module/providers.spec.ts b/packages/authentication/integration-tests/__tests__/services/module/providers.spec.ts index 607e6d0801c9d..3a49d6c3b0217 100644 --- a/packages/authentication/integration-tests/__tests__/services/module/providers.spec.ts +++ b/packages/authentication/integration-tests/__tests__/services/module/providers.spec.ts @@ -23,6 +23,10 @@ describe("AuthenticationModuleService - AuthProvider", () => { schema: process.env.MEDUSA_PRICING_DB_SCHEMA, }, }) + + if(service.__hooks?.onApplicationStart) { + await service.__hooks.onApplicationStart() + } }) afterEach(async () => { diff --git a/packages/authentication/src/loaders/providers.ts b/packages/authentication/src/loaders/providers.ts index c9ff2ee18fa52..0ffa6eee8f979 100644 --- a/packages/authentication/src/loaders/providers.ts +++ b/packages/authentication/src/loaders/providers.ts @@ -2,13 +2,10 @@ import * as defaultProviders from "@providers" import { LoaderOptions, ModulesSdkTypes } from "@medusajs/types" -import { AuthProviderService } from "@services" -import { ServiceTypes } from "@types" -import { asClass } from "awilix" +import { AwilixContainer, ClassOrFunctionReturning, Resolver, asClass, asFunction, asValue } from "awilix" export default async ({ container, - options, }: LoaderOptions< | ModulesSdkTypes.ModuleServiceInitializeOptions | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions @@ -19,40 +16,22 @@ export default async ({ const providersToLoad = Object.values(defaultProviders) - const authProviderService: AuthProviderService = container.resolve( - "authProviderService" - ) - let providers - - try { - providers = await authProviderService.list({ - provider: providersToLoad.map((p) => p.PROVIDER), - }) - } catch (error) { - if (error.name === "TableNotFoundException") { - // we are running loaders in migrations (or fail at a later point) - return - } - } - - const loadedProviders = new Map(providers.map((p) => [p.provider, p])) - - const providersToCreate: ServiceTypes.CreateAuthProviderDTO[] = [] - for (const provider of providersToLoad) { container.register({ [`auth_provider_${provider.PROVIDER}`]: asClass(provider).singleton(), }) + } - if (loadedProviders.has(provider.PROVIDER)) { - continue - } + container.register({ + [`auth_providers`]: asArray(providersToLoad), + }) +} - providersToCreate.push({ - provider: provider.PROVIDER, - name: provider.DISPLAY_NAME, - }) +function asArray( + resolvers: (ClassOrFunctionReturning | Resolver)[] +): { resolve: (container: AwilixContainer) => unknown[] } { + return { + resolve: (container: AwilixContainer) => + resolvers.map((resolver) => container.build(resolver)), } - - await authProviderService.create(providersToCreate) } diff --git a/packages/authentication/src/services/authentication-module.ts b/packages/authentication/src/services/authentication-module.ts index 86ed10be7b617..7638dc8595fc8 100644 --- a/packages/authentication/src/services/authentication-module.ts +++ b/packages/authentication/src/services/authentication-module.ts @@ -14,6 +14,7 @@ import { AuthProvider, AuthUser } from "@models" import { joinerConfig } from "../joiner-config" import { AuthProviderService, AuthUserService } from "@services" + import { InjectManager, InjectTransactionManager, @@ -29,6 +30,7 @@ import { FilterableAuthUserProps, UpdateAuthUserDTO, } from "@medusajs/types/dist/authentication/common" +import { ServiceTypes } from "@types" type InjectedDependencies = { baseRepository: DAL.RepositoryService @@ -45,6 +47,33 @@ export default class AuthenticationModuleService< return joinerConfig } + __hooks = { + onApplicationStart: async () => { + const providersToLoad = this.__container__["auth_providers"] + + const providers = await this.authProviderService_.list({ + provider: providersToLoad.map((p) => p.provider), + }) + + const loadedProvidersMap = new Map(providers.map((p) => [p.provider, p])) + + const providersToCreate: ServiceTypes.CreateAuthProviderDTO[] = [] + + for (const provider of providersToLoad) { + if (loadedProvidersMap.has(provider.provider)) { + continue + } + + providersToCreate.push({ + provider: provider.provider, + name: provider.displayName, + }) + } + + await this.authProviderService_.create(providersToCreate) + }, + } + protected __container__: MedusaContainer protected baseRepository_: DAL.RepositoryService @@ -351,10 +380,8 @@ export default class AuthenticationModuleService< ): AbstractAuthenticationModuleProvider { let containerProvider: AbstractAuthenticationModuleProvider try { - console.log("containerProvider") containerProvider = this.__container__[`auth_provider_${provider}`] } catch (error) { - console.log(error) throw new MedusaError( MedusaError.Types.NOT_FOUND, `AuthenticationProvider with for provider: ${provider} wasn't registered in the module. Have you configured your options correctly?` @@ -369,18 +396,16 @@ export default class AuthenticationModuleService< provider: string, authenticationData: Record, @MedusaContext() sharedContext: Context = {} - ): Promise { - let registeredProvider - console.log("hello") - try { + ): Promise { + let registeredProvider + + try { await this.retrieveAuthProvider(provider, {}) - console.log(registeredProvider) registeredProvider = this.getRegisteredAuthenticationProvider(provider) - + return await registeredProvider.authenticate(authenticationData) - } catch (error) { - console.log(JSON.stringify(error, null, 2)) + } catch (error) { return { success: false, error: error.message } } } diff --git a/packages/types/src/authentication/provider.ts b/packages/types/src/authentication/provider.ts index 7c2e0bafd3e76..b0be42d02b0d2 100644 --- a/packages/types/src/authentication/provider.ts +++ b/packages/types/src/authentication/provider.ts @@ -4,6 +4,22 @@ export abstract class AbstractAuthenticationModuleProvider { public static PROVIDER: string public static DISPLAY_NAME: string + public get provider() { + return (this.constructor as Function & { PROVIDER: string}).PROVIDER + } + + public get displayName() { + return (this.constructor as Function & { DISPLAY_NAME: string}).DISPLAY_NAME + } + + public getProvider(): string { + return (this.constructor as Function & { PROVIDER: string}).PROVIDER + } + + public getDisplayName(): string { + return (this.constructor as Function & { DISPLAY_NAME: string, PROVIDER: string}).PROVIDER + } + abstract authenticate( data: Record ): Promise From 8b2f0f1fa7eec9cf04d42d0c5db1c84c89fc2b9c Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 10:34:14 +0100 Subject: [PATCH 10/53] cleanup provider class --- packages/types/src/authentication/provider.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/types/src/authentication/provider.ts b/packages/types/src/authentication/provider.ts index b0be42d02b0d2..8a709a6db93cc 100644 --- a/packages/types/src/authentication/provider.ts +++ b/packages/types/src/authentication/provider.ts @@ -12,14 +12,6 @@ export abstract class AbstractAuthenticationModuleProvider { return (this.constructor as Function & { DISPLAY_NAME: string}).DISPLAY_NAME } - public getProvider(): string { - return (this.constructor as Function & { PROVIDER: string}).PROVIDER - } - - public getDisplayName(): string { - return (this.constructor as Function & { DISPLAY_NAME: string, PROVIDER: string}).PROVIDER - } - abstract authenticate( data: Record ): Promise From eda5647ddee829e83f4ec764cc9f54a6f35875b4 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 10:38:26 +0100 Subject: [PATCH 11/53] run application start hook before each --- .../__tests__/services/providers/username-password.spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts b/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts index 93621858baab4..7902c1591c894 100644 --- a/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts +++ b/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts @@ -29,6 +29,10 @@ describe("AuthenticationModuleService - AuthProvider", () => { schema: process.env.MEDUSA_PRICING_DB_SCHEMA, }, }) + + if(service.__hooks?.onApplicationStart) { + await service.__hooks.onApplicationStart() + } }) afterEach(async () => { From 3e99dffef57e95333e2a3a846cfbe22a7c41c42b Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Mon, 22 Jan 2024 10:37:19 +0700 Subject: [PATCH 12/53] fix pr feedback --- packages/authentication/package.json | 3 +- .../src/providers/username-password.ts | 7 +-- .../src/services/authentication-module.ts | 52 ++++++++++--------- 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/packages/authentication/package.json b/packages/authentication/package.json index f128ae7bb9ea6..8fbf500cb8610 100644 --- a/packages/authentication/package.json +++ b/packages/authentication/package.json @@ -29,8 +29,7 @@ "prepublishOnly": "cross-env NODE_ENV=production tsc --build && tsc-alias -p tsconfig.json", "build": "rimraf dist && tsc --build && tsc-alias -p tsconfig.json", "test": "jest --runInBand --bail --forceExit -- src/**/__tests__/**/*.ts", - "test:integration": "jest --runInBand --forceExit -- ", - "test:integration:single": "jest --runInBand --forceExit ", + "test:integration": "jest --runInBand --forceExit -- integration-tests/**/__tests__/**/*.ts", "migration:generate": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:generate", "migration:initial": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create --initial", "migration:create": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create", diff --git a/packages/authentication/src/providers/username-password.ts b/packages/authentication/src/providers/username-password.ts index b77c716f1174b..95c82fbcc61e7 100644 --- a/packages/authentication/src/providers/username-password.ts +++ b/packages/authentication/src/providers/username-password.ts @@ -5,6 +5,7 @@ import { import { AuthUserService } from "@services" import Scrypt from "scrypt-kdf" +import { isString } from "@medusajs/utils" class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { public static PROVIDER = "usernamePassword" @@ -23,14 +24,14 @@ class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { ): Promise { const { email, password } = userData.body - if (typeof password !== "string") { + if (isString(password)) { return { success: false, error: "Password should be a string", } } - if (typeof email !== "string") { + if (isString(email)) { return { success: false, error: "Email should be a string", @@ -44,7 +45,7 @@ class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { const password_hash = authUser.provider_metadata?.password - if (password_hash && typeof password_hash === "string") { + if (isString(password_hash)) { const buf = Buffer.from(password_hash, "base64") const success = await Scrypt.verify(buf, password) diff --git a/packages/authentication/src/services/authentication-module.ts b/packages/authentication/src/services/authentication-module.ts index 7638dc8595fc8..30d50d74a510c 100644 --- a/packages/authentication/src/services/authentication-module.ts +++ b/packages/authentication/src/services/authentication-module.ts @@ -48,32 +48,10 @@ export default class AuthenticationModuleService< } __hooks = { - onApplicationStart: async () => { - const providersToLoad = this.__container__["auth_providers"] - - const providers = await this.authProviderService_.list({ - provider: providersToLoad.map((p) => p.provider), - }) - - const loadedProvidersMap = new Map(providers.map((p) => [p.provider, p])) - - const providersToCreate: ServiceTypes.CreateAuthProviderDTO[] = [] - - for (const provider of providersToLoad) { - if (loadedProvidersMap.has(provider.provider)) { - continue - } - - providersToCreate.push({ - provider: provider.provider, - name: provider.displayName, - }) - } - - await this.authProviderService_.create(providersToCreate) - }, + onApplicationStart: this.createProvidersOnLoad, } + protected __container__: MedusaContainer protected baseRepository_: DAL.RepositoryService @@ -409,4 +387,30 @@ export default class AuthenticationModuleService< return { success: false, error: error.message } } } + + + private async createProvidersOnLoad() { + const providersToLoad = this.__container__["auth_providers"] + + const providers = await this.authProviderService_.list({ + provider: providersToLoad.map((p) => p.provider), + }) + + const loadedProvidersMap = new Map(providers.map((p) => [p.provider, p])) + + const providersToCreate: ServiceTypes.CreateAuthProviderDTO[] = [] + + for (const provider of providersToLoad) { + if (loadedProvidersMap.has(provider.provider)) { + continue + } + + providersToCreate.push({ + provider: provider.provider, + name: provider.displayName, + }) + } + + await this.authProviderService_.create(providersToCreate) + } } From 5bf05cc309f0d3ae0655e4a44e9f0979bb117271 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Mon, 22 Jan 2024 17:49:58 +0800 Subject: [PATCH 13/53] create private onApplicationStart method --- packages/authentication/src/providers/username-password.ts | 4 ++-- packages/authentication/src/services/authentication-module.ts | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/authentication/src/providers/username-password.ts b/packages/authentication/src/providers/username-password.ts index 95c82fbcc61e7..c6428253dc311 100644 --- a/packages/authentication/src/providers/username-password.ts +++ b/packages/authentication/src/providers/username-password.ts @@ -24,14 +24,14 @@ class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { ): Promise { const { email, password } = userData.body - if (isString(password)) { + if (!password || !isString(password)) { return { success: false, error: "Password should be a string", } } - if (isString(email)) { + if (!email || !isString(email)) { return { success: false, error: "Email should be a string", diff --git a/packages/authentication/src/services/authentication-module.ts b/packages/authentication/src/services/authentication-module.ts index 30d50d74a510c..93365943b3788 100644 --- a/packages/authentication/src/services/authentication-module.ts +++ b/packages/authentication/src/services/authentication-module.ts @@ -48,10 +48,9 @@ export default class AuthenticationModuleService< } __hooks = { - onApplicationStart: this.createProvidersOnLoad, + onApplicationStart: async () => await this.createProvidersOnLoad(), } - protected __container__: MedusaContainer protected baseRepository_: DAL.RepositoryService From 306c97269541ee04cfb6d7aa11c31963b732b8d6 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Mon, 22 Jan 2024 18:27:55 +0800 Subject: [PATCH 14/53] assign repository --- packages/authentication/src/services/auth-user.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/authentication/src/services/auth-user.ts b/packages/authentication/src/services/auth-user.ts index c85dda861e782..8ee6285386225 100644 --- a/packages/authentication/src/services/auth-user.ts +++ b/packages/authentication/src/services/auth-user.ts @@ -24,6 +24,7 @@ export default class AuthUserService< constructor(container: InjectedDependencies) { // @ts-ignore super(...arguments) + this.authUserRepository_ = container.authUserRepository } @InjectManager("authUserRepository_") From 80e70128c110b6d9e9b4b32176af7a98d91ab028 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 10:29:19 +0100 Subject: [PATCH 15/53] init --- packages/modules-sdk/src/medusa-module.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/modules-sdk/src/medusa-module.ts b/packages/modules-sdk/src/medusa-module.ts index 6144d93ff80f7..74ebdc04135fb 100644 --- a/packages/modules-sdk/src/medusa-module.ts +++ b/packages/modules-sdk/src/medusa-module.ts @@ -90,6 +90,10 @@ export class MedusaModule { public static onApplicationStart(): void { for (const instances of MedusaModule.instances_.values()) { for (const instance of Object.values(instances) as IModuleService[]) { +<<<<<<< HEAD +======= + console.log(instance) +>>>>>>> 01a9804c98 (init) if (instance?.__hooks) { instance.__hooks?.onApplicationStart ?.bind(instance)() From e7bd8cd55f0ce6a3d9605c98dc0152cf92352293 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Mon, 15 Jan 2024 15:26:41 +0100 Subject: [PATCH 16/53] add entity_id and update provider --- .../services/providers/username-password.spec.ts | 13 ++++++------- packages/authentication/src/loaders/providers.ts | 3 +-- packages/modules-sdk/src/medusa-module.ts | 4 ---- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts b/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts index 7902c1591c894..b8556ea1aa572 100644 --- a/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts +++ b/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts @@ -1,13 +1,12 @@ -import { SqlEntityManager } from "@mikro-orm/postgresql" -import Scrypt from "scrypt-kdf" - -import { MikroOrmWrapper } from "../../../utils" -import { initialize } from "../../../../src" import { DB_URL } from "@medusajs/pricing/integration-tests/utils" -import { MedusaModule } from "@medusajs/modules-sdk" import { IAuthenticationModuleService } from "@medusajs/types" -import { createAuthUsers } from "../../../__fixtures__/auth-user" +import { MedusaModule } from "@medusajs/modules-sdk" +import { MikroOrmWrapper } from "../../../utils" +import Scrypt from "scrypt-kdf" +import { SqlEntityManager } from "@mikro-orm/postgresql" import { createAuthProviders } from "../../../__fixtures__/auth-provider" +import { createAuthUsers } from "../../../__fixtures__/auth-user" +import { initialize } from "../../../../src" jest.setTimeout(30000) const seedDefaultData = async (testManager) => { diff --git a/packages/authentication/src/loaders/providers.ts b/packages/authentication/src/loaders/providers.ts index 0ffa6eee8f979..d68813c2aaac6 100644 --- a/packages/authentication/src/loaders/providers.ts +++ b/packages/authentication/src/loaders/providers.ts @@ -1,8 +1,7 @@ import * as defaultProviders from "@providers" -import { LoaderOptions, ModulesSdkTypes } from "@medusajs/types" - import { AwilixContainer, ClassOrFunctionReturning, Resolver, asClass, asFunction, asValue } from "awilix" +import { LoaderOptions, ModulesSdkTypes } from "@medusajs/types" export default async ({ container, diff --git a/packages/modules-sdk/src/medusa-module.ts b/packages/modules-sdk/src/medusa-module.ts index 74ebdc04135fb..6144d93ff80f7 100644 --- a/packages/modules-sdk/src/medusa-module.ts +++ b/packages/modules-sdk/src/medusa-module.ts @@ -90,10 +90,6 @@ export class MedusaModule { public static onApplicationStart(): void { for (const instances of MedusaModule.instances_.values()) { for (const instance of Object.values(instances) as IModuleService[]) { -<<<<<<< HEAD -======= - console.log(instance) ->>>>>>> 01a9804c98 (init) if (instance?.__hooks) { instance.__hooks?.onApplicationStart ?.bind(instance)() From 465cfa7d92ffc4616ea97bf55575d5d59f815d67 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 07:00:14 +0100 Subject: [PATCH 17/53] initial implementation --- packages/authentication/package.json | 2 + .../authentication/src/loaders/providers.ts | 12 +- .../.snapshot-medusa-authentication.json | 9 + .../src/migrations/Migration20240115150119.ts | 13 + .../src/models/auth-provider.ts | 4 + .../authentication/src/providers/google.ts | 273 ++++++++++++++++++ .../authentication/src/providers/index.ts | 1 + .../src/types/services/auth-provider.ts | 1 + .../src/types/services/auth-user.ts | 2 +- 9 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 packages/authentication/src/migrations/Migration20240115150119.ts create mode 100644 packages/authentication/src/providers/google.ts diff --git a/packages/authentication/package.json b/packages/authentication/package.json index 8fbf500cb8610..69e8356176e38 100644 --- a/packages/authentication/package.json +++ b/packages/authentication/package.json @@ -56,7 +56,9 @@ "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.0", "dotenv": "^16.1.4", + "jsonwebtoken": "^9.0.2", "knex": "2.4.2", + "oauth": "^0.10.0", "scrypt-kdf": "^2.0.1" } } diff --git a/packages/authentication/src/loaders/providers.ts b/packages/authentication/src/loaders/providers.ts index d68813c2aaac6..49cea3348344c 100644 --- a/packages/authentication/src/loaders/providers.ts +++ b/packages/authentication/src/loaders/providers.ts @@ -1,6 +1,12 @@ import * as defaultProviders from "@providers" -import { AwilixContainer, ClassOrFunctionReturning, Resolver, asClass, asFunction, asValue } from "awilix" +import { + AwilixContainer, + ClassOrFunctionReturning, + Constructor, + Resolver, + asClass, +} from "awilix" import { LoaderOptions, ModulesSdkTypes } from "@medusajs/types" export default async ({ @@ -17,7 +23,9 @@ export default async ({ for (const provider of providersToLoad) { container.register({ - [`auth_provider_${provider.PROVIDER}`]: asClass(provider).singleton(), + [`auth_provider_${provider.PROVIDER}`]: asClass( + provider as Constructor + ).singleton(), }) } diff --git a/packages/authentication/src/migrations/.snapshot-medusa-authentication.json b/packages/authentication/src/migrations/.snapshot-medusa-authentication.json index f7ebcf17c1ad9..c8b0c9c5ceb10 100644 --- a/packages/authentication/src/migrations/.snapshot-medusa-authentication.json +++ b/packages/authentication/src/migrations/.snapshot-medusa-authentication.json @@ -39,6 +39,15 @@ ], "mappedType": "enum" }, + "config": { + "name": "config", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "json" + }, "is_active": { "name": "is_active", "type": "boolean", diff --git a/packages/authentication/src/migrations/Migration20240115150119.ts b/packages/authentication/src/migrations/Migration20240115150119.ts new file mode 100644 index 0000000000000..44234347907b6 --- /dev/null +++ b/packages/authentication/src/migrations/Migration20240115150119.ts @@ -0,0 +1,13 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20240115150119 extends Migration { + + async up(): Promise { + this.addSql('alter table "auth_provider" add column "config" jsonb null;'); + } + + async down(): Promise { + this.addSql('alter table "auth_provider" drop column "config";'); + } + +} diff --git a/packages/authentication/src/models/auth-provider.ts b/packages/authentication/src/models/auth-provider.ts index 8febe9ff6f62f..204a7628266d3 100644 --- a/packages/authentication/src/models/auth-provider.ts +++ b/packages/authentication/src/models/auth-provider.ts @@ -5,6 +5,7 @@ import { PrimaryKey, Property, } from "@mikro-orm/core" + import { ProviderDomain } from "../types/repositories/auth-provider" type OptionalFields = "domain" | "is_active" @@ -22,6 +23,9 @@ export default class AuthProvider { @Enum({ items: () => ProviderDomain, default: ProviderDomain.ALL }) domain: ProviderDomain = ProviderDomain.ALL + @Property({ columnType: "jsonb", nullable: true }) + config: Record | null + @Property({ columnType: "boolean", default: false }) is_active = false } diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts new file mode 100644 index 0000000000000..4bd18995955f7 --- /dev/null +++ b/packages/authentication/src/providers/google.ts @@ -0,0 +1,273 @@ +import { + AbstractAuthenticationModuleProvider, + AuthenticationResponse, +} from "@medusajs/types" +import { AuthProviderService, AuthUserService } from "@services" + +import { OAuth2 } from "oauth" +import url, { UrlWithParsedQuery } from "url" +import jwt, { JwtPayload } from "jsonwebtoken" +import { MedusaError } from "@medusajs/utils" + +type InjectedDependencies = { + authUserService: AuthUserService + authProviderService: AuthProviderService +} + +type GoogleProviderConfig = { + callbackURL: string + clientID: string + clientSecret: string +} +const nullStore = { + store: function (req, cb) { + return cb() + }, + verify: function (req, state, cb) { + return cb(null, true) + }, +} + +class GoogleProvider extends AbstractAuthenticationModuleProvider { + public static PROVIDER = "google" + public static DISPLAY_NAME = "Google Authentication" + + private authorizationUrl_ = "https://accounts.google.com/o/oauth2/v2/auth" + private tokenUrl_ = "https://www.googleapis.com/oauth2/v4/token" + + protected readonly authUserSerivce_: AuthUserService + protected readonly authProviderService_: AuthProviderService + protected readonly _stateStore = nullStore + + constructor({ authUserService, authProviderService }: InjectedDependencies) { + super() + + this.authUserSerivce_ = authUserService + this.authProviderService_ = authProviderService + } + + private async validateConfig(config: Partial) { + if (!config.clientID) { + throw new Error("Google clientID is required") + } + + if (!config.clientSecret) { + throw new Error("Google clientSecret is required") + } + + if (!config.callbackURL) { + throw new Error("Google callbackUrl is required") + } + } + + async authenticate( + userData: Record + ): Promise { + const originalURL = (req) => { + var tls = req.connection.encrypted, + host = req.headers.host, + protocol = tls ? "https" : "http", + path = req.url || "" + return protocol + "://" + host + path + } + + const verify_ = async (request, accessToken, refreshToken, profile, done) => { + // decode email from jwt + const jwtData = await jwt.decode(refreshToken.id_token, { complete: true }) as JwtPayload | null + // const email = jwtData!.email + const entity_id = jwtData!.payload.email + + let authUser + + try { + authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( + entity_id, + GoogleProvider.PROVIDER + ) + } catch (error) { + if(error.type === MedusaError.Types.NOT_FOUND) { + authUser = await this.authUserSerivce_.create([{ + entity_id, + provider_id: GoogleProvider.PROVIDER, + user_metadata: { + email: jwtData!.payload.email, + email_verified: jwtData!.payload.email_verified, + name: jwtData!.payload.name, + picture: jwtData!.payload.picture, + locale: jwtData!.payload.locale, + family_name: jwtData!.payload.family_name, + given_name: jwtData!.payload.given_name, + hd: jwtData!.payload.hd, + }, + }]) + } + else { + return done(error.message) + } + } + + return done(null, authUser) + } + + const req = userData + + const provider = await this.authProviderService_.retrieve( + GoogleProvider.PROVIDER + ) + + try { + this.validateConfig(provider.config || {}) + } catch (error) { + return { success: false, error: error.message } + } + + let { callbackURL, clientID, clientSecret } = + provider.config as GoogleProviderConfig + + if (req.query && req.query.error) { + return { + success: false, + error: `${req.query.error_description}, read more at: ${req.query.error_uri}`, + } + } + + // var callbackURL = provider./callbackURL; + if (callbackURL) { + var parsed = url.parse(callbackURL) + if (!parsed.protocol) { + // The callback URL is relative, resolve a fully qualified URL from the + // URL of the originating request. + callbackURL = url.resolve(originalURL(req), callbackURL) + } + } + + var meta = { + authorizationURL: this.authorizationUrl_, + tokenURL: this.tokenUrl_, + clientID, + callbackURL: callbackURL, + } + + const oauth2 = new OAuth2( + clientID, + clientSecret, + "", + meta.authorizationURL, + meta.tokenURL + ) + + if ((req.query && req.query.code) || (req.body && req.body.code)) { + const stateStore = { + state: null, + + setState: function (state) { + this.state = state + }, + } + + function loaded(err, ok) { + if (err) { + stateStore.setState({ + success: false, + error: err, + }) + } + if (!ok) { + // return self.fail(state, 403); + // TODO: fail with status + return stateStore.setState({ + success: false, + error: "fail with status, 403", + }) + } + + var code = (req.query && req.query.code) || (req.body && req.body.code) + + var params: { + grant_type: string + redirect_uri?: string + code_verifier?: string + } = { grant_type: "authorization_code" } + if (callbackURL) { + params.redirect_uri = callbackURL + } + + oauth2.getOAuthAccessToken( + code, + params, + async (err, accessToken, refreshToken, params) => { + console.warn("testing") + if (err || !accessToken) { + return stateStore.setState({ + success: false, + error: "Failed to obtain access token", + }) + } + + function verified(err, user, info) { + if (err) { + stateStore.setState({ success: false, error: err }) + } else if (!user) { + stateStore.setState({ success: false, error: info }) + } else { + stateStore.setState({ success: true, authUser: user }) + } + } + + try { + await verify_(accessToken, refreshToken, params, {}, verified) + } catch (ex) { + stateStore.setState({ error: ex.message, success: false }) + } + } + ) + } + + var state = (req.query && req.query.state) || (req.body && req.body.state) + try { + this._stateStore.verify(req, state, loaded) + } catch (ex) { + return { success: false, error: ex } + } + + while(stateStore.state === null) { + await new Promise((resolve) => setTimeout(resolve, 100)) + } + + return stateStore.state + + } else { + var params: { + response_type: string + redirect_uri?: string + scope?: string + } = { response_type: "code", scope: "email profile" } + if (callbackURL) { + params.redirect_uri = callbackURL + } + + function stored(err, state) { + var parsed: Omit & { + search?: string | null + } = url.parse(oauth2._authorizeUrl, true) + for (const key in params) { + parsed.query[key] = params[key] + } + + parsed.query["client_id"] = meta.clientID + + delete parsed.search + var location = url.format(parsed) + return { success: true, location } // TODO: redirect + } + + try { + return this._stateStore.store(req, stored) + } catch (ex) { + return { success: false, error: ex } + } + } + } +} + +export default GoogleProvider diff --git a/packages/authentication/src/providers/index.ts b/packages/authentication/src/providers/index.ts index 77a2d444f782a..e00b4b172759d 100644 --- a/packages/authentication/src/providers/index.ts +++ b/packages/authentication/src/providers/index.ts @@ -1 +1,2 @@ export { default as UsernamePasswordProvider } from "./username-password" +export { default as GoogleProvider } from "./google" \ No newline at end of file diff --git a/packages/authentication/src/types/services/auth-provider.ts b/packages/authentication/src/types/services/auth-provider.ts index 8f1307ab07c29..d3419451c540c 100644 --- a/packages/authentication/src/types/services/auth-provider.ts +++ b/packages/authentication/src/types/services/auth-provider.ts @@ -10,6 +10,7 @@ export type CreateAuthProviderDTO = { name: string domain?: ProviderDomain is_active?: boolean + config?: Record } export type UpdateAuthProviderDTO = { diff --git a/packages/authentication/src/types/services/auth-user.ts b/packages/authentication/src/types/services/auth-user.ts index c059e980f8b9c..5e9898023fd8d 100644 --- a/packages/authentication/src/types/services/auth-user.ts +++ b/packages/authentication/src/types/services/auth-user.ts @@ -11,7 +11,7 @@ export type AuthUserDTO = { } export type CreateAuthUserDTO = { - entity_id: string + entity_id?: string provider_id: string provider_metadata?: Record user_metadata?: Record From 4bb7889ca705ad04b6a1b57199b71432782aaf82 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 07:03:07 +0100 Subject: [PATCH 18/53] update lockfile --- yarn.lock | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/yarn.lock b/yarn.lock index dcfec06735e9f..b095cd87b9338 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7854,8 +7854,10 @@ __metadata: cross-env: ^5.2.1 dotenv: ^16.1.4 jest: ^29.6.3 + jsonwebtoken: ^9.0.2 knex: 2.4.2 medusa-test-utils: ^1.1.40 + oauth: ^0.10.0 rimraf: ^3.0.2 scrypt-kdf: ^2.0.1 ts-jest: ^29.1.1 @@ -35407,6 +35409,24 @@ __metadata: languageName: node linkType: hard +"jsonwebtoken@npm:^9.0.2": + version: 9.0.2 + resolution: "jsonwebtoken@npm:9.0.2" + dependencies: + jws: ^3.2.2 + lodash.includes: ^4.3.0 + lodash.isboolean: ^3.0.3 + lodash.isinteger: ^4.0.4 + lodash.isnumber: ^3.0.3 + lodash.isplainobject: ^4.0.6 + lodash.isstring: ^4.0.1 + lodash.once: ^4.0.0 + ms: ^2.1.1 + semver: ^7.5.4 + checksum: d287a29814895e866db2e5a0209ce730cbc158441a0e5a70d5e940eb0d28ab7498c6bf45029cc8b479639bca94056e9a7f254e2cdb92a2f5750c7f358657a131 + languageName: node + linkType: hard + "jsprim@npm:^1.2.2": version: 1.4.2 resolution: "jsprim@npm:1.4.2" @@ -39440,6 +39460,13 @@ __metadata: languageName: node linkType: hard +"oauth@npm:^0.10.0": + version: 0.10.0 + resolution: "oauth@npm:0.10.0" + checksum: 76f3e186cfd76cb33e5d5d442861c86680a5c3b71b2db1b854212087532c265a69de1a2ab9db683e6c6df733e17cfc67476527b81b224a19c1917de2bc3f75fa + languageName: node + linkType: hard + "obj-case@npm:0.2.1": version: 0.2.1 resolution: "obj-case@npm:0.2.1" From 1ffff8ab75f368216d9bfe08bedc97d19b0aceaa Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 10:40:58 +0100 Subject: [PATCH 19/53] fix conflicts --- packages/authentication/src/types/services/auth-user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/authentication/src/types/services/auth-user.ts b/packages/authentication/src/types/services/auth-user.ts index 5e9898023fd8d..c059e980f8b9c 100644 --- a/packages/authentication/src/types/services/auth-user.ts +++ b/packages/authentication/src/types/services/auth-user.ts @@ -11,7 +11,7 @@ export type AuthUserDTO = { } export type CreateAuthUserDTO = { - entity_id?: string + entity_id: string provider_id: string provider_metadata?: Record user_metadata?: Record From 0b1f784995f243fb182a84dcf0bed3b8e5cb436e Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 10:43:28 +0100 Subject: [PATCH 20/53] add config variables --- packages/authentication/src/types/repositories/auth-provider.ts | 2 ++ packages/authentication/src/types/services/auth-provider.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/authentication/src/types/repositories/auth-provider.ts b/packages/authentication/src/types/repositories/auth-provider.ts index 31f5ce087a5f9..6f1ea0dc88e0f 100644 --- a/packages/authentication/src/types/repositories/auth-provider.ts +++ b/packages/authentication/src/types/repositories/auth-provider.ts @@ -5,6 +5,7 @@ export type CreateAuthProviderDTO = { name: string domain?: ProviderDomain is_active?: boolean + config?: Record } export type UpdateAuthProviderDTO = { @@ -13,6 +14,7 @@ export type UpdateAuthProviderDTO = { name?: string domain?: ProviderDomain is_active?: boolean + config?: Record } provider: AuthProvider } diff --git a/packages/authentication/src/types/services/auth-provider.ts b/packages/authentication/src/types/services/auth-provider.ts index d3419451c540c..8ef5d9b3b9bdd 100644 --- a/packages/authentication/src/types/services/auth-provider.ts +++ b/packages/authentication/src/types/services/auth-provider.ts @@ -3,6 +3,7 @@ export type AuthProviderDTO = { name: string domain: ProviderDomain is_active: boolean + config: Record } export type CreateAuthProviderDTO = { @@ -18,6 +19,7 @@ export type UpdateAuthProviderDTO = { name?: string domain?: ProviderDomain is_active?: boolean + config?: Record } export enum ProviderDomain { From 620d93dccd46d82260c0201a1fb58366b7491a81 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 10:50:51 +0100 Subject: [PATCH 21/53] update types --- packages/authentication/src/models/auth-provider.ts | 2 +- packages/authentication/src/providers/google.ts | 4 ++-- packages/authentication/src/services/authentication-module.ts | 4 ++-- packages/types/src/authentication/common/auth-provider.ts | 3 +++ 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/authentication/src/models/auth-provider.ts b/packages/authentication/src/models/auth-provider.ts index 204a7628266d3..29a0d241c5390 100644 --- a/packages/authentication/src/models/auth-provider.ts +++ b/packages/authentication/src/models/auth-provider.ts @@ -24,7 +24,7 @@ export default class AuthProvider { domain: ProviderDomain = ProviderDomain.ALL @Property({ columnType: "jsonb", nullable: true }) - config: Record | null + config: Record | null @Property({ columnType: "boolean", default: false }) is_active = false diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index 4bd18995955f7..1bb1080380157 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -225,7 +225,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { var state = (req.query && req.query.state) || (req.body && req.body.state) try { - this._stateStore.verify(req, state, loaded) + this._stateStore.verify(req, state, loaded) // TODO: call loaded } catch (ex) { return { success: false, error: ex } } @@ -246,7 +246,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { params.redirect_uri = callbackURL } - function stored(err, state) { + function stored() { var parsed: Omit & { search?: string | null } = url.parse(oauth2._authorizeUrl, true) diff --git a/packages/authentication/src/services/authentication-module.ts b/packages/authentication/src/services/authentication-module.ts index 93365943b3788..ddf4498b81700 100644 --- a/packages/authentication/src/services/authentication-module.ts +++ b/packages/authentication/src/services/authentication-module.ts @@ -158,7 +158,7 @@ export default class AuthenticationModuleService< protected async createAuthProviders_( data: any[], @MedusaContext() sharedContext: Context - ): Promise { + ): Promise { return await this.authProviderService_.create(data, sharedContext) } @@ -196,7 +196,7 @@ export default class AuthenticationModuleService< async updateAuthProvider_( data: AuthenticationTypes.UpdateAuthProviderDTO[], @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise { return await this.authProviderService_.update(data, sharedContext) } diff --git a/packages/types/src/authentication/common/auth-provider.ts b/packages/types/src/authentication/common/auth-provider.ts index f39d01b21755f..e3653642136f8 100644 --- a/packages/types/src/authentication/common/auth-provider.ts +++ b/packages/types/src/authentication/common/auth-provider.ts @@ -5,6 +5,7 @@ export type AuthProviderDTO = { name: string domain: ProviderDomain is_active: boolean + config: Record | null } export type CreateAuthProviderDTO = { @@ -12,6 +13,7 @@ export type CreateAuthProviderDTO = { name: string domain?: ProviderDomain is_active?: boolean + config?: Record } export type UpdateAuthProviderDTO = { @@ -19,6 +21,7 @@ export type UpdateAuthProviderDTO = { name?: string domain?: ProviderDomain is_active?: boolean + config?: Record } export enum ProviderDomain { From 9e916dc1c8c47cced9c693a99a8e69de33b8b12c Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 14:00:23 +0100 Subject: [PATCH 22/53] refactor google provider --- .../authentication/src/providers/google.ts | 340 +++++++++--------- 1 file changed, 170 insertions(+), 170 deletions(-) diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index 1bb1080380157..b7f407ab1f217 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -8,6 +8,7 @@ import { OAuth2 } from "oauth" import url, { UrlWithParsedQuery } from "url" import jwt, { JwtPayload } from "jsonwebtoken" import { MedusaError } from "@medusajs/utils" +import { TreeLevelColumn } from "typeorm" type InjectedDependencies = { authUserService: AuthUserService @@ -19,14 +20,6 @@ type GoogleProviderConfig = { clientID: string clientSecret: string } -const nullStore = { - store: function (req, cb) { - return cb() - }, - verify: function (req, state, cb) { - return cb(null, true) - }, -} class GoogleProvider extends AbstractAuthenticationModuleProvider { public static PROVIDER = "google" @@ -37,7 +30,6 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { protected readonly authUserSerivce_: AuthUserService protected readonly authProviderService_: AuthProviderService - protected readonly _stateStore = nullStore constructor({ authUserService, authProviderService }: InjectedDependencies) { super() @@ -60,69 +52,68 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { } } - async authenticate( - userData: Record - ): Promise { - const originalURL = (req) => { - var tls = req.connection.encrypted, - host = req.headers.host, - protocol = tls ? "https" : "http", - path = req.url || "" - return protocol + "://" + host + path - } + private originalURL(req) { + var tls = req.connection.encrypted, + host = req.headers.host, + protocol = tls ? "https" : "http", + path = req.url || "" + return protocol + "://" + host + path + } + + async verify_(request, accessToken, refreshToken) { + // decode email from jwt + const jwtData = (await jwt.decode(refreshToken.id_token, { + complete: true, + })) as JwtPayload | null + // const email = jwtData!.email + const entity_id = jwtData!.payload.email + + let authUser - const verify_ = async (request, accessToken, refreshToken, profile, done) => { - // decode email from jwt - const jwtData = await jwt.decode(refreshToken.id_token, { complete: true }) as JwtPayload | null - // const email = jwtData!.email - const entity_id = jwtData!.payload.email - - let authUser - - try { - authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( - entity_id, - GoogleProvider.PROVIDER - ) - } catch (error) { - if(error.type === MedusaError.Types.NOT_FOUND) { - authUser = await this.authUserSerivce_.create([{ + try { + authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( + entity_id, + GoogleProvider.PROVIDER + ) + } catch (error) { + if (error.type === MedusaError.Types.NOT_FOUND) { + authUser = await this.authUserSerivce_.create([ + { entity_id, provider_id: GoogleProvider.PROVIDER, - user_metadata: { - email: jwtData!.payload.email, - email_verified: jwtData!.payload.email_verified, - name: jwtData!.payload.name, - picture: jwtData!.payload.picture, - locale: jwtData!.payload.locale, - family_name: jwtData!.payload.family_name, - given_name: jwtData!.payload.given_name, - hd: jwtData!.payload.hd, - }, - }]) - } - else { - return done(error.message) - } + user_metadata: jwtData!.payload, + }, + ]) + } else { + return { success: false, error: error.message } } - - return done(null, authUser) } - const req = userData + return { success: true, authUser } + } - const provider = await this.authProviderService_.retrieve( + async getProviderConfig( + req: Record + ): Promise { + const { config } = await this.authProviderService_.retrieve( GoogleProvider.PROVIDER ) - try { - this.validateConfig(provider.config || {}) - } catch (error) { - return { success: false, error: error.message } - } + this.validateConfig(config || {}) + + const { callbackURL } = config as GoogleProviderConfig + + const cbUrl = !url.parse(callbackURL).protocol + ? url.resolve(this.originalURL(req), callbackURL) + : callbackURL - let { callbackURL, clientID, clientSecret } = - provider.config as GoogleProviderConfig + return {...config, callbackURL: cbUrl } as GoogleProviderConfig + } + + async authenticate( + userData: Record + ): Promise { + const req = userData if (req.query && req.query.error) { return { @@ -131,141 +122,150 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { } } - // var callbackURL = provider./callbackURL; - if (callbackURL) { - var parsed = url.parse(callbackURL) - if (!parsed.protocol) { - // The callback URL is relative, resolve a fully qualified URL from the - // URL of the originating request. - callbackURL = url.resolve(originalURL(req), callbackURL) - } + let config + + try { + config = await this.getProviderConfig(req) + } catch (error) { + return { success: false, error: error.message } } + let { callbackURL, clientID, clientSecret } = config + var meta = { authorizationURL: this.authorizationUrl_, tokenURL: this.tokenUrl_, clientID, - callbackURL: callbackURL, + callbackURL, + clientSecret, + } + + const code = (req.query && req.query.code) || (req.body && req.body.code) + + // Redirect to google + if (!code) { + return this.getRedirect(meta) } + return await this.validateCallback(code, meta) + } + + // abstractable + private async validateCallback(code: string, { + authorizationURL, + tokenURL, + clientID, + callbackURL, + clientSecret, + }: { + authorizationURL: string + tokenURL: string + clientID: string + callbackURL: string + clientSecret: string + }) { const oauth2 = new OAuth2( clientID, clientSecret, "", - meta.authorizationURL, - meta.tokenURL + authorizationURL, + tokenURL ) - if ((req.query && req.query.code) || (req.body && req.body.code)) { - const stateStore = { - state: null, + let state = null + const setState = (newState) => { + state = newState + } + + try { + oauth2.getOAuthAccessToken( + code, + { grant_type: "authorization_code", redirect_uri: callbackURL }, + this.getOAuthAccessTokenCallback(setState) + ) + } catch (ex) { + return { success: false, error: ex } + } - setState: function (state) { - this.state = state - }, - } + // wait for callback to resolve + while (state === null) { + await new Promise((resolve) => setTimeout(resolve, 50)) + } - function loaded(err, ok) { - if (err) { - stateStore.setState({ - success: false, - error: err, - }) - } - if (!ok) { - // return self.fail(state, 403); - // TODO: fail with status - return stateStore.setState({ - success: false, - error: "fail with status, 403", - }) - } - - var code = (req.query && req.query.code) || (req.body && req.body.code) - - var params: { - grant_type: string - redirect_uri?: string - code_verifier?: string - } = { grant_type: "authorization_code" } - if (callbackURL) { - params.redirect_uri = callbackURL - } - - oauth2.getOAuthAccessToken( - code, - params, - async (err, accessToken, refreshToken, params) => { - console.warn("testing") - if (err || !accessToken) { - return stateStore.setState({ - success: false, - error: "Failed to obtain access token", - }) - } - - function verified(err, user, info) { - if (err) { - stateStore.setState({ success: false, error: err }) - } else if (!user) { - stateStore.setState({ success: false, error: info }) - } else { - stateStore.setState({ success: true, authUser: user }) - } - } - - try { - await verify_(accessToken, refreshToken, params, {}, verified) - } catch (ex) { - stateStore.setState({ error: ex.message, success: false }) - } - } - ) - } + return state + } - var state = (req.query && req.query.state) || (req.body && req.body.state) - try { - this._stateStore.verify(req, state, loaded) // TODO: call loaded - } catch (ex) { - return { success: false, error: ex } - } + private getOAuthAccessTokenCallback(setResult) { + return async (err, accessToken, refreshToken, params) => { + const result = await this.oathAccessTokenCallback( + err, + accessToken, + refreshToken, + params + ) + setResult(result) + } + } - while(stateStore.state === null) { - await new Promise((resolve) => setTimeout(resolve, 100)) + // abstractable + private async oathAccessTokenCallback( + err, + accessToken, + refreshToken, + params + ) { + if (err || !accessToken) { + return { + success: false, + error: "Failed to obtain access token", } + } + + try { + return await this.verify_(accessToken, refreshToken, params) + } catch (ex) { + return { error: ex.message, success: false } + } + } - return stateStore.state + // Abstractable + private getRedirect({ + authorizationURL, + clientID, + callbackURL + }: { + authorizationURL: string + clientID: string + callbackURL: string + }) { + const params: { + response_type: string + redirect_uri: string + scope?: string + } = { + response_type: "code", + scope: "email profile", + redirect_uri: callbackURL, + } - } else { - var params: { - response_type: string - redirect_uri?: string - scope?: string - } = { response_type: "code", scope: "email profile" } - if (callbackURL) { - params.redirect_uri = callbackURL + try { + const parsed: Omit & { + search?: string | null + } = url.parse(authorizationURL, true) + + for (const key in params) { + parsed.query[key] = params[key] } - function stored() { - var parsed: Omit & { - search?: string | null - } = url.parse(oauth2._authorizeUrl, true) - for (const key in params) { - parsed.query[key] = params[key] - } + parsed.query["client_id"] = clientID - parsed.query["client_id"] = meta.clientID + delete parsed.search - delete parsed.search - var location = url.format(parsed) - return { success: true, location } // TODO: redirect - } + var location = url.format(parsed) - try { - return this._stateStore.store(req, stored) - } catch (ex) { - return { success: false, error: ex } - } + return { success: true, location } + } catch (ex) { + return { success: false, error: ex.message } } } } From b4bf54046abb2a9436ae245e6c3fa9a954da3e1d Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 14:01:15 +0100 Subject: [PATCH 23/53] re-order methods --- .../authentication/src/providers/google.ts | 66 ++++++++++--------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index b7f407ab1f217..10686b32b64df 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -60,38 +60,6 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { return protocol + "://" + host + path } - async verify_(request, accessToken, refreshToken) { - // decode email from jwt - const jwtData = (await jwt.decode(refreshToken.id_token, { - complete: true, - })) as JwtPayload | null - // const email = jwtData!.email - const entity_id = jwtData!.payload.email - - let authUser - - try { - authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( - entity_id, - GoogleProvider.PROVIDER - ) - } catch (error) { - if (error.type === MedusaError.Types.NOT_FOUND) { - authUser = await this.authUserSerivce_.create([ - { - entity_id, - provider_id: GoogleProvider.PROVIDER, - user_metadata: jwtData!.payload, - }, - ]) - } else { - return { success: false, error: error.message } - } - } - - return { success: true, authUser } - } - async getProviderConfig( req: Record ): Promise { @@ -195,6 +163,40 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { return state } + // abstractable + async verify_(request, accessToken, refreshToken) { + // decode email from jwt + const jwtData = (await jwt.decode(refreshToken.id_token, { + complete: true, + })) as JwtPayload | null + // const email = jwtData!.email + const entity_id = jwtData!.payload.email + + let authUser + + try { + authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( + entity_id, + GoogleProvider.PROVIDER + ) + } catch (error) { + if (error.type === MedusaError.Types.NOT_FOUND) { + authUser = await this.authUserSerivce_.create([ + { + entity_id, + provider_id: GoogleProvider.PROVIDER, + user_metadata: jwtData!.payload, + }, + ]) + } else { + return { success: false, error: error.message } + } + } + + return { success: true, authUser } + } + + private getOAuthAccessTokenCallback(setResult) { return async (err, accessToken, refreshToken, params) => { const result = await this.oathAccessTokenCallback( From 4aa760fb0c47f93c90e426b4d155064e82803f45 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Mon, 22 Jan 2024 10:44:10 +0700 Subject: [PATCH 24/53] fix pr feedback p. 1 --- .../.snapshot-medusa-authentication.json | 2 +- .../src/migrations/Migration20240115092929.ts | 15 ----- .../src/migrations/Migration20240115150119.ts | 13 ----- ...04154451.ts => Migration20240122041959.ts} | 7 ++- .../src/models/auth-provider.ts | 4 +- .../authentication/src/providers/google.ts | 58 ++++++++++++------- 6 files changed, 44 insertions(+), 55 deletions(-) delete mode 100644 packages/authentication/src/migrations/Migration20240115092929.ts delete mode 100644 packages/authentication/src/migrations/Migration20240115150119.ts rename packages/authentication/src/migrations/{Migration20240104154451.ts => Migration20240122041959.ts} (56%) diff --git a/packages/authentication/src/migrations/.snapshot-medusa-authentication.json b/packages/authentication/src/migrations/.snapshot-medusa-authentication.json index c8b0c9c5ceb10..a31fe796e7234 100644 --- a/packages/authentication/src/migrations/.snapshot-medusa-authentication.json +++ b/packages/authentication/src/migrations/.snapshot-medusa-authentication.json @@ -45,7 +45,7 @@ "unsigned": false, "autoincrement": false, "primary": false, - "nullable": false, + "nullable": true, "mappedType": "json" }, "is_active": { diff --git a/packages/authentication/src/migrations/Migration20240115092929.ts b/packages/authentication/src/migrations/Migration20240115092929.ts deleted file mode 100644 index d06392934456e..0000000000000 --- a/packages/authentication/src/migrations/Migration20240115092929.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Migration } from '@mikro-orm/migrations'; - -export class Migration20240115092929 extends Migration { - - async up(): Promise { - this.addSql('alter table "auth_user" add column "entity_id" text not null;'); - this.addSql('alter table "auth_user" add constraint "IDX_auth_user_provider_entity_id" unique ("provider_id", "entity_id");'); - } - - async down(): Promise { - this.addSql('alter table "auth_user" drop constraint "IDX_auth_user_provider_entity_id";'); - this.addSql('alter table "auth_user" drop column "entity_id";'); - } - -} diff --git a/packages/authentication/src/migrations/Migration20240115150119.ts b/packages/authentication/src/migrations/Migration20240115150119.ts deleted file mode 100644 index 44234347907b6..0000000000000 --- a/packages/authentication/src/migrations/Migration20240115150119.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Migration } from '@mikro-orm/migrations'; - -export class Migration20240115150119 extends Migration { - - async up(): Promise { - this.addSql('alter table "auth_provider" add column "config" jsonb null;'); - } - - async down(): Promise { - this.addSql('alter table "auth_provider" drop column "config";'); - } - -} diff --git a/packages/authentication/src/migrations/Migration20240104154451.ts b/packages/authentication/src/migrations/Migration20240122041959.ts similarity index 56% rename from packages/authentication/src/migrations/Migration20240104154451.ts rename to packages/authentication/src/migrations/Migration20240122041959.ts index 2ffaf00de1668..e4bcc513f218d 100644 --- a/packages/authentication/src/migrations/Migration20240104154451.ts +++ b/packages/authentication/src/migrations/Migration20240122041959.ts @@ -1,11 +1,12 @@ import { Migration } from '@mikro-orm/migrations'; -export class Migration20240104154451 extends Migration { +export class Migration20240122041959 extends Migration { async up(): Promise { - this.addSql('create table "auth_provider" ("provider" text not null, "name" text not null, "domain" text check ("domain" in (\'all\', \'store\', \'admin\')) not null default \'all\', "is_active" boolean not null default false, constraint "auth_provider_pkey" primary key ("provider"));'); + this.addSql('create table "auth_provider" ("provider" text not null, "name" text not null, "domain" text check ("domain" in (\'all\', \'store\', \'admin\')) not null default \'all\', "config" jsonb null, "is_active" boolean not null default false, constraint "auth_provider_pkey" primary key ("provider"));'); - this.addSql('create table "auth_user" ("id" text not null, "provider_id" text null, "user_metadata" jsonb null, "app_metadata" jsonb null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));'); + this.addSql('create table "auth_user" ("id" text not null, "entity_id" text not null, "provider_id" text null, "user_metadata" jsonb null, "app_metadata" jsonb null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));'); + this.addSql('alter table "auth_user" add constraint "IDX_auth_user_provider_entity_id" unique ("provider_id", "entity_id");'); this.addSql('alter table "auth_user" add constraint "auth_user_provider_id_foreign" foreign key ("provider_id") references "auth_provider" ("provider") on delete cascade;'); } diff --git a/packages/authentication/src/models/auth-provider.ts b/packages/authentication/src/models/auth-provider.ts index 29a0d241c5390..0827186069bbd 100644 --- a/packages/authentication/src/models/auth-provider.ts +++ b/packages/authentication/src/models/auth-provider.ts @@ -8,7 +8,7 @@ import { import { ProviderDomain } from "../types/repositories/auth-provider" -type OptionalFields = "domain" | "is_active" +type OptionalFields = "domain" | "is_active" | "config" @Entity() export default class AuthProvider { @@ -24,7 +24,7 @@ export default class AuthProvider { domain: ProviderDomain = ProviderDomain.ALL @Property({ columnType: "jsonb", nullable: true }) - config: Record | null + config: Record | null = null @Property({ columnType: "boolean", default: false }) is_active = false diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index 10686b32b64df..4206a70375394 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -3,11 +3,12 @@ import { AuthenticationResponse, } from "@medusajs/types" import { AuthProviderService, AuthUserService } from "@services" - -import { OAuth2 } from "oauth" -import url, { UrlWithParsedQuery } from "url" import jwt, { JwtPayload } from "jsonwebtoken" +import url, { UrlWithParsedQuery } from "url" + +import { AuthProvider } from "@models" import { MedusaError } from "@medusajs/utils" +import { OAuth2 } from "oauth" import { TreeLevelColumn } from "typeorm" type InjectedDependencies = { @@ -25,8 +26,9 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { public static PROVIDER = "google" public static DISPLAY_NAME = "Google Authentication" - private authorizationUrl_ = "https://accounts.google.com/o/oauth2/v2/auth" - private tokenUrl_ = "https://www.googleapis.com/oauth2/v4/token" + // TODO: abstract + private readonly authorizationUrl_ = "https://accounts.google.com/o/oauth2/v2/auth" + private readonly tokenUrl_ = "https://www.googleapis.com/oauth2/v4/token" protected readonly authUserSerivce_: AuthUserService protected readonly authProviderService_: AuthProviderService @@ -53,7 +55,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { } private originalURL(req) { - var tls = req.connection.encrypted, + const tls = req.connection.encrypted, host = req.headers.host, protocol = tls ? "https" : "http", path = req.url || "" @@ -63,26 +65,24 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { async getProviderConfig( req: Record ): Promise { - const { config } = await this.authProviderService_.retrieve( + const { config } = (await this.authProviderService_.retrieve( GoogleProvider.PROVIDER - ) + )) as AuthProvider & { config: GoogleProviderConfig } this.validateConfig(config || {}) - const { callbackURL } = config as GoogleProviderConfig + const { callbackURL } = config - const cbUrl = !url.parse(callbackURL).protocol + const parsedCallbackUrl = !url.parse(callbackURL).protocol ? url.resolve(this.originalURL(req), callbackURL) : callbackURL - return {...config, callbackURL: cbUrl } as GoogleProviderConfig + return {...config, callbackURL: parsedCallbackUrl } } async authenticate( - userData: Record + req: Record ): Promise { - const req = userData - if (req.query && req.query.error) { return { success: false, @@ -100,7 +100,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { let { callbackURL, clientID, clientSecret } = config - var meta = { + const meta = { authorizationURL: this.authorizationUrl_, tokenURL: this.tokenUrl_, clientID, @@ -141,6 +141,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { ) let state = null + const setState = (newState) => { state = newState } @@ -151,6 +152,22 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { { grant_type: "authorization_code", redirect_uri: callbackURL }, this.getOAuthAccessTokenCallback(setState) ) + // await new Promise((resolve, reject) => { + // oauth2.getOAuthAccessToken( + // code, + // { grant_type: "authorization_code", redirect_uri: callbackURL }, + // (err, accessToken, refreshToken, params) => { + // return this.getOAuthAccessTokenCallback(setState)( + // err, + // accessToken, + // refreshToken, + // params + // ) + // .catch(reject) + // .finally(() => resolve()) + // } + // ) + // }) } catch (ex) { return { success: false, error: ex } } @@ -168,9 +185,8 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { // decode email from jwt const jwtData = (await jwt.decode(refreshToken.id_token, { complete: true, - })) as JwtPayload | null - // const email = jwtData!.email - const entity_id = jwtData!.payload.email + })) as JwtPayload + const entity_id = jwtData.payload.email let authUser @@ -199,7 +215,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { private getOAuthAccessTokenCallback(setResult) { return async (err, accessToken, refreshToken, params) => { - const result = await this.oathAccessTokenCallback( + const result = await this.oAuthAccessTokenCallback( err, accessToken, refreshToken, @@ -210,7 +226,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { } // abstractable - private async oathAccessTokenCallback( + private async oAuthAccessTokenCallback( err, accessToken, refreshToken, @@ -263,7 +279,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { delete parsed.search - var location = url.format(parsed) + const location = url.format(parsed) return { success: true, location } } catch (ex) { From 63a85c97644c9723a58ab425ba114e90c9f9f1d1 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Mon, 22 Jan 2024 17:22:22 +0800 Subject: [PATCH 25/53] add initial type and update callback authorization --- .../authentication/src/providers/google.ts | 165 ++++++++++-------- 1 file changed, 90 insertions(+), 75 deletions(-) diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index 4206a70375394..82b31ce741313 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -22,12 +22,21 @@ type GoogleProviderConfig = { clientSecret: string } +type AuthenticationInput = { + connection: { encrypted: boolean } + url: string + headers:{ host: string } + query: Record + body: Record +} + class GoogleProvider extends AbstractAuthenticationModuleProvider { public static PROVIDER = "google" public static DISPLAY_NAME = "Google Authentication" // TODO: abstract - private readonly authorizationUrl_ = "https://accounts.google.com/o/oauth2/v2/auth" + private readonly authorizationUrl_ = + "https://accounts.google.com/o/oauth2/v2/auth" private readonly tokenUrl_ = "https://www.googleapis.com/oauth2/v4/token" protected readonly authUserSerivce_: AuthUserService @@ -54,7 +63,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { } } - private originalURL(req) { + private originalURL(req: AuthenticationInput) { const tls = req.connection.encrypted, host = req.headers.host, protocol = tls ? "https" : "http", @@ -63,7 +72,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { } async getProviderConfig( - req: Record + req: AuthenticationInput ): Promise { const { config } = (await this.authProviderService_.retrieve( GoogleProvider.PROVIDER @@ -77,11 +86,11 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { ? url.resolve(this.originalURL(req), callbackURL) : callbackURL - return {...config, callbackURL: parsedCallbackUrl } + return { ...config, callbackURL: parsedCallbackUrl } } async authenticate( - req: Record + req: AuthenticationInput ): Promise { if (req.query && req.query.error) { return { @@ -119,19 +128,22 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { } // abstractable - private async validateCallback(code: string, { - authorizationURL, - tokenURL, - clientID, - callbackURL, - clientSecret, - }: { - authorizationURL: string - tokenURL: string - clientID: string - callbackURL: string - clientSecret: string - }) { + private async validateCallback( + code: string, + { + authorizationURL, + tokenURL, + clientID, + callbackURL, + clientSecret, + }: { + authorizationURL: string + tokenURL: string + clientID: string + callbackURL: string + clientSecret: string + } + ) { const oauth2 = new OAuth2( clientID, clientSecret, @@ -141,77 +153,80 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { ) let state = null - + const setState = (newState) => { state = newState } - + try { - oauth2.getOAuthAccessToken( - code, - { grant_type: "authorization_code", redirect_uri: callbackURL }, - this.getOAuthAccessTokenCallback(setState) - ) - // await new Promise((resolve, reject) => { - // oauth2.getOAuthAccessToken( - // code, - // { grant_type: "authorization_code", redirect_uri: callbackURL }, - // (err, accessToken, refreshToken, params) => { - // return this.getOAuthAccessTokenCallback(setState)( - // err, - // accessToken, - // refreshToken, - // params - // ) - // .catch(reject) - // .finally(() => resolve()) - // } - // ) - // }) + // oauth2.getOAuthAccessToken( + // code, + // { grant_type: "authorization_code", redirect_uri: callbackURL }, + // this.getOAuthAccessTokenCallback(setState) + // ) + await new Promise((resolve, reject) => { + oauth2.getOAuthAccessToken( + code, + { grant_type: "authorization_code", redirect_uri: callbackURL }, + (err, accessToken, refreshToken, params) => { + return this.getOAuthAccessTokenCallback(setState)( + err, + accessToken, + refreshToken, + params + ) + .catch(reject) + .finally(() => resolve()) + } + ) + }) } catch (ex) { return { success: false, error: ex } } - // wait for callback to resolve - while (state === null) { - await new Promise((resolve) => setTimeout(resolve, 50)) + if(!state) { + return { success: false, error: "Authentication failed"} } + // // wait for callback to resolve + // while (state === null) { + // await new Promise((resolve) => setTimeout(resolve, 50)) + // } + return state } - // abstractable - async verify_(request, accessToken, refreshToken) { - // decode email from jwt - const jwtData = (await jwt.decode(refreshToken.id_token, { - complete: true, - })) as JwtPayload - const entity_id = jwtData.payload.email - - let authUser - - try { - authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( - entity_id, - GoogleProvider.PROVIDER - ) - } catch (error) { - if (error.type === MedusaError.Types.NOT_FOUND) { - authUser = await this.authUserSerivce_.create([ - { - entity_id, - provider_id: GoogleProvider.PROVIDER, - user_metadata: jwtData!.payload, - }, - ]) - } else { - return { success: false, error: error.message } - } + // abstractable + async verify_(request, accessToken, refreshToken) { + // decode email from jwt + const jwtData = (await jwt.decode(refreshToken.id_token, { + complete: true, + })) as JwtPayload + const entity_id = jwtData.payload.email + + let authUser + + try { + authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( + entity_id, + GoogleProvider.PROVIDER + ) + } catch (error) { + if (error.type === MedusaError.Types.NOT_FOUND) { + authUser = await this.authUserSerivce_.create([ + { + entity_id, + provider_id: GoogleProvider.PROVIDER, + user_metadata: jwtData!.payload, + }, + ]) + } else { + return { success: false, error: error.message } } - - return { success: true, authUser } } - + + return { success: true, authUser } + } private getOAuthAccessTokenCallback(setResult) { return async (err, accessToken, refreshToken, params) => { @@ -250,7 +265,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { private getRedirect({ authorizationURL, clientID, - callbackURL + callbackURL, }: { authorizationURL: string clientID: string From c8745046a5026b07c9d6ca1428d7699a388c23ea Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Mon, 22 Jan 2024 18:36:34 +0800 Subject: [PATCH 26/53] add google provider to integration test --- .../services/module/providers.spec.ts | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/authentication/integration-tests/__tests__/services/module/providers.spec.ts b/packages/authentication/integration-tests/__tests__/services/module/providers.spec.ts index 3a49d6c3b0217..cefb712ce166e 100644 --- a/packages/authentication/integration-tests/__tests__/services/module/providers.spec.ts +++ b/packages/authentication/integration-tests/__tests__/services/module/providers.spec.ts @@ -1,11 +1,10 @@ -import { SqlEntityManager } from "@mikro-orm/postgresql" - -import { MikroOrmWrapper } from "../../../utils" -import { initialize } from "../../../../src" import { DB_URL } from "@medusajs/pricing/integration-tests/utils" -import { MedusaModule } from "@medusajs/modules-sdk" import { IAuthenticationModuleService } from "@medusajs/types" +import { MedusaModule } from "@medusajs/modules-sdk" +import { MikroOrmWrapper } from "../../../utils" +import { SqlEntityManager } from "@mikro-orm/postgresql" import { createAuthProviders } from "../../../__fixtures__/auth-provider" +import { initialize } from "../../../../src" jest.setTimeout(30000) @@ -24,7 +23,7 @@ describe("AuthenticationModuleService - AuthProvider", () => { }, }) - if(service.__hooks?.onApplicationStart) { + if (service.__hooks?.onApplicationStart) { await service.__hooks.onApplicationStart() } }) @@ -39,12 +38,18 @@ describe("AuthenticationModuleService - AuthProvider", () => { const authProviders = await service.listAuthProviders() const serialized = JSON.parse(JSON.stringify(authProviders)) - expect(serialized).toEqual([ - expect.objectContaining({ - provider: "usernamePassword", - name: "Username/Password Authentication", - }), - ]) + expect(serialized).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + provider: "usernamePassword", + name: "Username/Password Authentication", + }), + expect.objectContaining({ + provider: "google", + name: "Google Authentication", + }), + ]) + ) }) }) From 9a17515e23fe48799d3ba063ea947cf142d14f3f Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Tue, 23 Jan 2024 14:52:46 +0800 Subject: [PATCH 27/53] fix feedback --- .../src/migrations/Migration20240122041959.ts | 6 +++--- packages/authentication/src/providers/google.ts | 10 ---------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/authentication/src/migrations/Migration20240122041959.ts b/packages/authentication/src/migrations/Migration20240122041959.ts index e4bcc513f218d..763930b944a9d 100644 --- a/packages/authentication/src/migrations/Migration20240122041959.ts +++ b/packages/authentication/src/migrations/Migration20240122041959.ts @@ -3,16 +3,16 @@ import { Migration } from '@mikro-orm/migrations'; export class Migration20240122041959 extends Migration { async up(): Promise { - this.addSql('create table "auth_provider" ("provider" text not null, "name" text not null, "domain" text check ("domain" in (\'all\', \'store\', \'admin\')) not null default \'all\', "config" jsonb null, "is_active" boolean not null default false, constraint "auth_provider_pkey" primary key ("provider"));'); + this.addSql('create table if not exists "auth_provider" ("provider" text not null, "name" text not null, "domain" text check ("domain" in (\'all\', \'store\', \'admin\')) not null default \'all\', "config" jsonb null, "is_active" boolean not null default false, constraint "auth_provider_pkey" primary key ("provider"));'); - this.addSql('create table "auth_user" ("id" text not null, "entity_id" text not null, "provider_id" text null, "user_metadata" jsonb null, "app_metadata" jsonb null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));'); + this.addSql('create table if not exists "auth_user" ("id" text not null, "entity_id" text not null, "provider_id" text null, "user_metadata" jsonb null, "app_metadata" jsonb null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));'); this.addSql('alter table "auth_user" add constraint "IDX_auth_user_provider_entity_id" unique ("provider_id", "entity_id");'); this.addSql('alter table "auth_user" add constraint "auth_user_provider_id_foreign" foreign key ("provider_id") references "auth_provider" ("provider") on delete cascade;'); } async down(): Promise { - this.addSql('alter table "auth_user" drop constraint "auth_user_provider_id_foreign";'); + this.addSql('alter table "auth_user" drop constraint if exists "auth_user_provider_id_foreign";'); this.addSql('drop table if exists "auth_provider" cascade;'); diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index 82b31ce741313..1e609f74b20bb 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -159,11 +159,6 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { } try { - // oauth2.getOAuthAccessToken( - // code, - // { grant_type: "authorization_code", redirect_uri: callbackURL }, - // this.getOAuthAccessTokenCallback(setState) - // ) await new Promise((resolve, reject) => { oauth2.getOAuthAccessToken( code, @@ -188,11 +183,6 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { return { success: false, error: "Authentication failed"} } - // // wait for callback to resolve - // while (state === null) { - // await new Promise((resolve) => setTimeout(resolve, 50)) - // } - return state } From 020b845b6a29d9fa25cc6826fc495aecfc1f1c1d Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Tue, 23 Jan 2024 16:28:46 +0800 Subject: [PATCH 28/53] initial implementation (#6171) * initial implementation * remove oauth lib --- packages/authentication/package.json | 4 +- .../authentication/src/providers/google.ts | 187 +++++------------- yarn.lock | 81 ++++++-- 3 files changed, 122 insertions(+), 150 deletions(-) diff --git a/packages/authentication/package.json b/packages/authentication/package.json index 69e8356176e38..7fee6905fe378 100644 --- a/packages/authentication/package.json +++ b/packages/authentication/package.json @@ -58,7 +58,7 @@ "dotenv": "^16.1.4", "jsonwebtoken": "^9.0.2", "knex": "2.4.2", - "oauth": "^0.10.0", - "scrypt-kdf": "^2.0.1" + "scrypt-kdf": "^2.0.1", + "simple-oauth2": "^5.0.0" } } diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index 1e609f74b20bb..6842d042eaf6b 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -4,41 +4,35 @@ import { } from "@medusajs/types" import { AuthProviderService, AuthUserService } from "@services" import jwt, { JwtPayload } from "jsonwebtoken" -import url, { UrlWithParsedQuery } from "url" +import url from "url" import { AuthProvider } from "@models" import { MedusaError } from "@medusajs/utils" -import { OAuth2 } from "oauth" -import { TreeLevelColumn } from "typeorm" +import { AuthorizationCode } from "simple-oauth2" type InjectedDependencies = { authUserService: AuthUserService authProviderService: AuthProviderService } -type GoogleProviderConfig = { - callbackURL: string - clientID: string - clientSecret: string -} - type AuthenticationInput = { connection: { encrypted: boolean } url: string - headers:{ host: string } + headers: { host: string } query: Record body: Record } +type ProviderConfig = { + clientID: string + clientSecret: string + callbackURL: string +} + class GoogleProvider extends AbstractAuthenticationModuleProvider { public static PROVIDER = "google" public static DISPLAY_NAME = "Google Authentication" - // TODO: abstract - private readonly authorizationUrl_ = - "https://accounts.google.com/o/oauth2/v2/auth" - private readonly tokenUrl_ = "https://www.googleapis.com/oauth2/v4/token" - protected readonly authUserSerivce_: AuthUserService protected readonly authProviderService_: AuthProviderService @@ -49,7 +43,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { this.authProviderService_ = authProviderService } - private async validateConfig(config: Partial) { + private async validateConfig(config: Partial) { if (!config.clientID) { throw new Error("Google clientID is required") } @@ -71,12 +65,10 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { return protocol + "://" + host + path } - async getProviderConfig( - req: AuthenticationInput - ): Promise { + async getProviderConfig(req: AuthenticationInput): Promise { const { config } = (await this.authProviderService_.retrieve( GoogleProvider.PROVIDER - )) as AuthProvider & { config: GoogleProviderConfig } + )) as AuthProvider & { config: ProviderConfig } this.validateConfig(config || {}) @@ -109,9 +101,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { let { callbackURL, clientID, clientSecret } = config - const meta = { - authorizationURL: this.authorizationUrl_, - tokenURL: this.tokenUrl_, + const meta: ProviderConfig = { clientID, callbackURL, clientSecret, @@ -130,66 +120,27 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { // abstractable private async validateCallback( code: string, - { - authorizationURL, - tokenURL, - clientID, - callbackURL, - clientSecret, - }: { - authorizationURL: string - tokenURL: string - clientID: string - callbackURL: string - clientSecret: string - } + { clientID, callbackURL, clientSecret }: ProviderConfig ) { - const oauth2 = new OAuth2( - clientID, - clientSecret, - "", - authorizationURL, - tokenURL - ) - - let state = null + const client = this.getAuthorizationCodeHandler({ clientID, clientSecret }) - const setState = (newState) => { - state = newState + const tokenParams = { + code, + redirect_uri: callbackURL, } try { - await new Promise((resolve, reject) => { - oauth2.getOAuthAccessToken( - code, - { grant_type: "authorization_code", redirect_uri: callbackURL }, - (err, accessToken, refreshToken, params) => { - return this.getOAuthAccessTokenCallback(setState)( - err, - accessToken, - refreshToken, - params - ) - .catch(reject) - .finally(() => resolve()) - } - ) - }) - } catch (ex) { - return { success: false, error: ex } - } + const accessToken = await client.getToken(tokenParams) - if(!state) { - return { success: false, error: "Authentication failed"} + return await this.verify_(accessToken.id_token) + } catch (error) { + return { success: false, error: error.message } } - - return state } // abstractable - async verify_(request, accessToken, refreshToken) { - // decode email from jwt - const jwtData = (await jwt.decode(refreshToken.id_token, { + async verify_(refreshToken: string) { + const jwtData = (await jwt.decode(refreshToken, { complete: true, })) as JwtPayload const entity_id = jwtData.payload.email @@ -218,78 +169,40 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { return { success: true, authUser } } - private getOAuthAccessTokenCallback(setResult) { - return async (err, accessToken, refreshToken, params) => { - const result = await this.oAuthAccessTokenCallback( - err, - accessToken, - refreshToken, - params - ) - setResult(result) - } - } + // Abstractable + private getRedirect({ clientID, callbackURL, clientSecret }: ProviderConfig) { + const client = this.getAuthorizationCodeHandler({ clientID, clientSecret }) - // abstractable - private async oAuthAccessTokenCallback( - err, - accessToken, - refreshToken, - params - ) { - if (err || !accessToken) { - return { - success: false, - error: "Failed to obtain access token", - } - } + const location = client.authorizeURL({ + redirect_uri: callbackURL, + scope: "email profile", + }) - try { - return await this.verify_(accessToken, refreshToken, params) - } catch (ex) { - return { error: ex.message, success: false } - } + return { success: true, location } } - // Abstractable - private getRedirect({ - authorizationURL, + private getAuthorizationCodeHandler({ clientID, - callbackURL, + clientSecret, }: { - authorizationURL: string clientID: string - callbackURL: string + clientSecret: string }) { - const params: { - response_type: string - redirect_uri: string - scope?: string - } = { - response_type: "code", - scope: "email profile", - redirect_uri: callbackURL, - } - - try { - const parsed: Omit & { - search?: string | null - } = url.parse(authorizationURL, true) - - for (const key in params) { - parsed.query[key] = params[key] - } - - parsed.query["client_id"] = clientID - - delete parsed.search - - const location = url.format(parsed) - - return { success: true, location } - } catch (ex) { - return { success: false, error: ex.message } - } + const config = { + client: { + id: clientID, + secret: clientSecret, + }, + auth: { + // TODO: abstract to not be google specific + authorizeHost: "https://accounts.google.com", + authorizePath: "/o/oauth2/v2/auth", + tokenHost: "https://www.googleapis.com", + tokenPath: "/oauth2/v4/token", + }, + } + + return new AuthorizationCode(config) } } diff --git a/yarn.lock b/yarn.lock index b095cd87b9338..98b24d02b9cb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6060,14 +6060,44 @@ __metadata: languageName: node linkType: hard -"@hapi/hoek@npm:^9.0.0": +"@hapi/boom@npm:^10.0.1": + version: 10.0.1 + resolution: "@hapi/boom@npm:10.0.1" + dependencies: + "@hapi/hoek": ^11.0.2 + checksum: e4ae8a69bb67c5687320d320a0706ac66e797a659c19fb1c9b909eaefe3b41780e4ecd4382de1297b10c33e9db81f79667324576b9153f57b0cf701293b908d0 + languageName: node + linkType: hard + +"@hapi/bourne@npm:^3.0.0": + version: 3.0.0 + resolution: "@hapi/bourne@npm:3.0.0" + checksum: 2e2df62f6bc6f32b980ba5bbdc09200c93c55c8306399ec0f2781da088a82aab699498c89fe94fec4acf770210f9aee28c75bfc2f04044849ac01b034134e717 + languageName: node + linkType: hard + +"@hapi/hoek@npm:^10.0.1": + version: 10.0.1 + resolution: "@hapi/hoek@npm:10.0.1" + checksum: 320d5dc7a4070fa29e6344a3af9e44854980c6606848f7b7f59715174880cc09a1fe1e8adf44cf887100bd8d6a8664e9dc415986b30dc91df13455f7114de549 + languageName: node + linkType: hard + +"@hapi/hoek@npm:^11.0.2": + version: 11.0.4 + resolution: "@hapi/hoek@npm:11.0.4" + checksum: 3c0e487824daaf3af4c29e46fd57b0c5801ce9164fef2417c70e271cd970e13cc542b196f70ba1cfc9ef944eed825fcac261085ab5e2928c6017428bf576b363 + languageName: node + linkType: hard + +"@hapi/hoek@npm:^9.0.0, @hapi/hoek@npm:^9.3.0": version: 9.3.0 resolution: "@hapi/hoek@npm:9.3.0" checksum: a096063805051fb8bba4c947e293c664b05a32b47e13bc654c0dd43813a1cec993bdd8f29ceb838020299e1d0f89f68dc0d62a603c13c9cc8541963f0beca055 languageName: node linkType: hard -"@hapi/topo@npm:^5.0.0": +"@hapi/topo@npm:^5.0.0, @hapi/topo@npm:^5.1.0": version: 5.1.0 resolution: "@hapi/topo@npm:5.1.0" dependencies: @@ -6076,6 +6106,17 @@ __metadata: languageName: node linkType: hard +"@hapi/wreck@npm:^18.0.0": + version: 18.0.1 + resolution: "@hapi/wreck@npm:18.0.1" + dependencies: + "@hapi/boom": ^10.0.1 + "@hapi/bourne": ^3.0.0 + "@hapi/hoek": ^11.0.2 + checksum: 46b1b1f750a66c4724964eb6d9192d1d19cfa45e602386aae76f52e3b423c9ae14a03a0f0e9f962e7d973708e1b0b6ab42d2ae77539a691fa77a18c78ccf285c + languageName: node + linkType: hard + "@headlessui/react@npm:^1.7.18": version: 1.7.18 resolution: "@headlessui/react@npm:1.7.18" @@ -7857,9 +7898,9 @@ __metadata: jsonwebtoken: ^9.0.2 knex: 2.4.2 medusa-test-utils: ^1.1.40 - oauth: ^0.10.0 rimraf: ^3.0.2 scrypt-kdf: ^2.0.1 + simple-oauth2: ^5.0.0 ts-jest: ^29.1.1 ts-node: ^10.9.1 tsc-alias: ^1.8.6 @@ -12140,7 +12181,7 @@ __metadata: languageName: node linkType: hard -"@sideway/address@npm:^4.1.3": +"@sideway/address@npm:^4.1.3, @sideway/address@npm:^4.1.4": version: 4.1.4 resolution: "@sideway/address@npm:4.1.4" dependencies: @@ -34940,6 +34981,19 @@ __metadata: languageName: node linkType: hard +"joi@npm:^17.6.4": + version: 17.12.0 + resolution: "joi@npm:17.12.0" + dependencies: + "@hapi/hoek": ^9.3.0 + "@hapi/topo": ^5.1.0 + "@sideway/address": ^4.1.4 + "@sideway/formula": ^3.0.1 + "@sideway/pinpoint": ^2.0.0 + checksum: 2378f4ec8de2bc12674ce3e6faac509f52ff4f734c67bf68c288816b20336d4e59433ea1c1e187f1009075c81ec5fa8b5061094feb37a855d6e3ee0cfcd79dd8 + languageName: node + linkType: hard + "join-component@npm:^1.1.0": version: 1.1.0 resolution: "join-component@npm:1.1.0" @@ -39460,13 +39514,6 @@ __metadata: languageName: node linkType: hard -"oauth@npm:^0.10.0": - version: 0.10.0 - resolution: "oauth@npm:0.10.0" - checksum: 76f3e186cfd76cb33e5d5d442861c86680a5c3b71b2db1b854212087532c265a69de1a2ab9db683e6c6df733e17cfc67476527b81b224a19c1917de2bc3f75fa - languageName: node - linkType: hard - "obj-case@npm:0.2.1": version: 0.2.1 resolution: "obj-case@npm:0.2.1" @@ -46018,6 +46065,18 @@ __metadata: languageName: node linkType: hard +"simple-oauth2@npm:^5.0.0": + version: 5.0.0 + resolution: "simple-oauth2@npm:5.0.0" + dependencies: + "@hapi/hoek": ^10.0.1 + "@hapi/wreck": ^18.0.0 + debug: ^4.3.4 + joi: ^17.6.4 + checksum: 1cb5a4eb9022f656e1bb9a1f43d771dd058d4a4fa181b42d0e1e7ca7b5cfc42e35fad1c722be9bb6fa218398b3b0499010554a7367d2bd85eb9d7634f92546c1 + languageName: node + linkType: hard + "simple-string-table@npm:^1.0.0": version: 1.0.0 resolution: "simple-string-table@npm:1.0.0" From dbac0ee6fadd2532ed999108237e9186bc62608d Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Tue, 23 Jan 2024 16:35:42 +0800 Subject: [PATCH 29/53] move abstract authentication provider --- packages/types/src/authentication/index.ts | 1 - .../authentication/abstract-authentication-provider.ts} | 7 ++++--- packages/utils/src/authentication/index.ts | 1 + packages/utils/src/index.ts | 1 + 4 files changed, 6 insertions(+), 4 deletions(-) rename packages/{types/src/authentication/provider.ts => utils/src/authentication/abstract-authentication-provider.ts} (76%) create mode 100644 packages/utils/src/authentication/index.ts diff --git a/packages/types/src/authentication/index.ts b/packages/types/src/authentication/index.ts index 711715ff6f885..1aa665fd541fe 100644 --- a/packages/types/src/authentication/index.ts +++ b/packages/types/src/authentication/index.ts @@ -1,3 +1,2 @@ export * from "./service" export * from "./common" -export * from "./provider" diff --git a/packages/types/src/authentication/provider.ts b/packages/utils/src/authentication/abstract-authentication-provider.ts similarity index 76% rename from packages/types/src/authentication/provider.ts rename to packages/utils/src/authentication/abstract-authentication-provider.ts index 8a709a6db93cc..52599feff85d1 100644 --- a/packages/types/src/authentication/provider.ts +++ b/packages/utils/src/authentication/abstract-authentication-provider.ts @@ -1,15 +1,16 @@ -import { AuthUserDTO } from "./common" +import { AuthUserDTO } from "@medusajs/types" export abstract class AbstractAuthenticationModuleProvider { public static PROVIDER: string public static DISPLAY_NAME: string public get provider() { - return (this.constructor as Function & { PROVIDER: string}).PROVIDER + return (this.constructor as Function & { PROVIDER: string }).PROVIDER } public get displayName() { - return (this.constructor as Function & { DISPLAY_NAME: string}).DISPLAY_NAME + return (this.constructor as Function & { DISPLAY_NAME: string }) + .DISPLAY_NAME } abstract authenticate( diff --git a/packages/utils/src/authentication/index.ts b/packages/utils/src/authentication/index.ts new file mode 100644 index 0000000000000..43c855ec7cf50 --- /dev/null +++ b/packages/utils/src/authentication/index.ts @@ -0,0 +1 @@ +export * from "./abstract-authentication-provider" diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 5f96dcb3dd3f6..d86097b681024 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,3 +1,4 @@ +export * from "./authentication" export * from "./bundles" export * from "./common" export * from "./dal" From fb3fef240eee699977ed3382d70460bee317803c Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Tue, 23 Jan 2024 16:51:40 +0800 Subject: [PATCH 30/53] shuffle files around --- packages/authentication/src/providers/google.ts | 10 +++++----- .../authentication/src/providers/username-password.ts | 7 ++----- .../src/services/authentication-module.ts | 7 +++---- packages/types/src/authentication/common/index.ts | 1 + packages/types/src/authentication/common/provider.ts | 5 +++++ packages/types/src/authentication/service.ts | 7 ++++--- .../authentication/abstract-authentication-provider.ts | 8 +------- 7 files changed, 21 insertions(+), 24 deletions(-) create mode 100644 packages/types/src/authentication/common/provider.ts diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index 6842d042eaf6b..a78c9ef74d010 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -1,13 +1,13 @@ -import { - AbstractAuthenticationModuleProvider, - AuthenticationResponse, -} from "@medusajs/types" +import { AuthenticationResponse } from "@medusajs/types" import { AuthProviderService, AuthUserService } from "@services" import jwt, { JwtPayload } from "jsonwebtoken" import url from "url" import { AuthProvider } from "@models" -import { MedusaError } from "@medusajs/utils" +import { + AbstractAuthenticationModuleProvider, + MedusaError, +} from "@medusajs/utils" import { AuthorizationCode } from "simple-oauth2" type InjectedDependencies = { diff --git a/packages/authentication/src/providers/username-password.ts b/packages/authentication/src/providers/username-password.ts index c6428253dc311..b19f26c5d7526 100644 --- a/packages/authentication/src/providers/username-password.ts +++ b/packages/authentication/src/providers/username-password.ts @@ -1,11 +1,8 @@ -import { - AbstractAuthenticationModuleProvider, - AuthenticationResponse, -} from "@medusajs/types" +import { AuthenticationResponse } from "@medusajs/types" import { AuthUserService } from "@services" import Scrypt from "scrypt-kdf" -import { isString } from "@medusajs/utils" +import { AbstractAuthenticationModuleProvider, isString } from "@medusajs/utils" class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { public static PROVIDER = "usernamePassword" diff --git a/packages/authentication/src/services/authentication-module.ts b/packages/authentication/src/services/authentication-module.ts index ddf4498b81700..f2fdf4948cd31 100644 --- a/packages/authentication/src/services/authentication-module.ts +++ b/packages/authentication/src/services/authentication-module.ts @@ -1,5 +1,4 @@ import { - AbstractAuthenticationModuleProvider, AuthenticationResponse, AuthenticationTypes, Context, @@ -16,6 +15,7 @@ import { joinerConfig } from "../joiner-config" import { AuthProviderService, AuthUserService } from "@services" import { + AbstractAuthenticationModuleProvider, InjectManager, InjectTransactionManager, MedusaContext, @@ -380,15 +380,14 @@ export default class AuthenticationModuleService< await this.retrieveAuthProvider(provider, {}) registeredProvider = this.getRegisteredAuthenticationProvider(provider) - + return await registeredProvider.authenticate(authenticationData) } catch (error) { return { success: false, error: error.message } } } - - private async createProvidersOnLoad() { + private async createProvidersOnLoad() { const providersToLoad = this.__container__["auth_providers"] const providers = await this.authProviderService_.list({ diff --git a/packages/types/src/authentication/common/index.ts b/packages/types/src/authentication/common/index.ts index b4282c985c6a3..332e78fe42975 100644 --- a/packages/types/src/authentication/common/index.ts +++ b/packages/types/src/authentication/common/index.ts @@ -1,2 +1,3 @@ export * from "./auth-user" export * from "./auth-provider" +export * from "./provider" diff --git a/packages/types/src/authentication/common/provider.ts b/packages/types/src/authentication/common/provider.ts new file mode 100644 index 0000000000000..aef4339630a86 --- /dev/null +++ b/packages/types/src/authentication/common/provider.ts @@ -0,0 +1,5 @@ +export type AuthenticationResponse = { + success: boolean + authUser?: any + error?: string +} diff --git a/packages/types/src/authentication/service.ts b/packages/types/src/authentication/service.ts index 64d63fb25e574..ddd0d76c6b4a6 100644 --- a/packages/types/src/authentication/service.ts +++ b/packages/types/src/authentication/service.ts @@ -1,5 +1,5 @@ -import { IModuleService } from "../modules-sdk" import { + AuthenticationResponse, AuthProviderDTO, AuthUserDTO, CreateAuthProviderDTO, @@ -9,9 +9,10 @@ import { UpdateAuthProviderDTO, UpdateAuthUserDTO, } from "./common" -import { FindConfig } from "../common" + import { Context } from "../shared-context" -import { AuthenticationResponse } from "./provider" +import { FindConfig } from "../common" +import { IModuleService } from "../modules-sdk" export interface IAuthenticationModuleService extends IModuleService { authenticate( diff --git a/packages/utils/src/authentication/abstract-authentication-provider.ts b/packages/utils/src/authentication/abstract-authentication-provider.ts index 52599feff85d1..f4d69ab1bd82d 100644 --- a/packages/utils/src/authentication/abstract-authentication-provider.ts +++ b/packages/utils/src/authentication/abstract-authentication-provider.ts @@ -1,4 +1,4 @@ -import { AuthUserDTO } from "@medusajs/types" +import { AuthenticationResponse } from "@medusajs/types"; export abstract class AbstractAuthenticationModuleProvider { public static PROVIDER: string @@ -17,9 +17,3 @@ export abstract class AbstractAuthenticationModuleProvider { data: Record ): Promise } - -export type AuthenticationResponse = { - success: boolean - authUser?: AuthUserDTO - error?: string -} From 08ba487a41c412b91a0da082b60a7facacec7681 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 10:29:19 +0100 Subject: [PATCH 31/53] init --- packages/modules-sdk/src/medusa-module.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/modules-sdk/src/medusa-module.ts b/packages/modules-sdk/src/medusa-module.ts index 6144d93ff80f7..74ebdc04135fb 100644 --- a/packages/modules-sdk/src/medusa-module.ts +++ b/packages/modules-sdk/src/medusa-module.ts @@ -90,6 +90,10 @@ export class MedusaModule { public static onApplicationStart(): void { for (const instances of MedusaModule.instances_.values()) { for (const instance of Object.values(instances) as IModuleService[]) { +<<<<<<< HEAD +======= + console.log(instance) +>>>>>>> 01a9804c98 (init) if (instance?.__hooks) { instance.__hooks?.onApplicationStart ?.bind(instance)() From 38ee8e35258635ac04cddf975ef3066c8b050c00 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Mon, 15 Jan 2024 15:26:41 +0100 Subject: [PATCH 32/53] add entity_id and update provider --- .../services/providers/username-password.spec.ts | 13 ++++++------- packages/authentication/src/loaders/providers.ts | 3 +-- packages/modules-sdk/src/medusa-module.ts | 4 ---- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts b/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts index 7902c1591c894..b8556ea1aa572 100644 --- a/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts +++ b/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts @@ -1,13 +1,12 @@ -import { SqlEntityManager } from "@mikro-orm/postgresql" -import Scrypt from "scrypt-kdf" - -import { MikroOrmWrapper } from "../../../utils" -import { initialize } from "../../../../src" import { DB_URL } from "@medusajs/pricing/integration-tests/utils" -import { MedusaModule } from "@medusajs/modules-sdk" import { IAuthenticationModuleService } from "@medusajs/types" -import { createAuthUsers } from "../../../__fixtures__/auth-user" +import { MedusaModule } from "@medusajs/modules-sdk" +import { MikroOrmWrapper } from "../../../utils" +import Scrypt from "scrypt-kdf" +import { SqlEntityManager } from "@mikro-orm/postgresql" import { createAuthProviders } from "../../../__fixtures__/auth-provider" +import { createAuthUsers } from "../../../__fixtures__/auth-user" +import { initialize } from "../../../../src" jest.setTimeout(30000) const seedDefaultData = async (testManager) => { diff --git a/packages/authentication/src/loaders/providers.ts b/packages/authentication/src/loaders/providers.ts index 0ffa6eee8f979..d68813c2aaac6 100644 --- a/packages/authentication/src/loaders/providers.ts +++ b/packages/authentication/src/loaders/providers.ts @@ -1,8 +1,7 @@ import * as defaultProviders from "@providers" -import { LoaderOptions, ModulesSdkTypes } from "@medusajs/types" - import { AwilixContainer, ClassOrFunctionReturning, Resolver, asClass, asFunction, asValue } from "awilix" +import { LoaderOptions, ModulesSdkTypes } from "@medusajs/types" export default async ({ container, diff --git a/packages/modules-sdk/src/medusa-module.ts b/packages/modules-sdk/src/medusa-module.ts index 74ebdc04135fb..6144d93ff80f7 100644 --- a/packages/modules-sdk/src/medusa-module.ts +++ b/packages/modules-sdk/src/medusa-module.ts @@ -90,10 +90,6 @@ export class MedusaModule { public static onApplicationStart(): void { for (const instances of MedusaModule.instances_.values()) { for (const instance of Object.values(instances) as IModuleService[]) { -<<<<<<< HEAD -======= - console.log(instance) ->>>>>>> 01a9804c98 (init) if (instance?.__hooks) { instance.__hooks?.onApplicationStart ?.bind(instance)() From 234302bf4e1d761eff555d6aaa21b6c090cc90d3 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 07:00:14 +0100 Subject: [PATCH 33/53] initial implementation --- packages/authentication/package.json | 2 + .../authentication/src/loaders/providers.ts | 12 +- .../.snapshot-medusa-authentication.json | 9 + .../src/migrations/Migration20240115150119.ts | 13 + .../src/models/auth-provider.ts | 4 + .../authentication/src/providers/google.ts | 273 ++++++++++++++++++ .../authentication/src/providers/index.ts | 1 + .../src/types/services/auth-provider.ts | 1 + .../src/types/services/auth-user.ts | 2 +- 9 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 packages/authentication/src/migrations/Migration20240115150119.ts create mode 100644 packages/authentication/src/providers/google.ts diff --git a/packages/authentication/package.json b/packages/authentication/package.json index 8fbf500cb8610..69e8356176e38 100644 --- a/packages/authentication/package.json +++ b/packages/authentication/package.json @@ -56,7 +56,9 @@ "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.0", "dotenv": "^16.1.4", + "jsonwebtoken": "^9.0.2", "knex": "2.4.2", + "oauth": "^0.10.0", "scrypt-kdf": "^2.0.1" } } diff --git a/packages/authentication/src/loaders/providers.ts b/packages/authentication/src/loaders/providers.ts index d68813c2aaac6..49cea3348344c 100644 --- a/packages/authentication/src/loaders/providers.ts +++ b/packages/authentication/src/loaders/providers.ts @@ -1,6 +1,12 @@ import * as defaultProviders from "@providers" -import { AwilixContainer, ClassOrFunctionReturning, Resolver, asClass, asFunction, asValue } from "awilix" +import { + AwilixContainer, + ClassOrFunctionReturning, + Constructor, + Resolver, + asClass, +} from "awilix" import { LoaderOptions, ModulesSdkTypes } from "@medusajs/types" export default async ({ @@ -17,7 +23,9 @@ export default async ({ for (const provider of providersToLoad) { container.register({ - [`auth_provider_${provider.PROVIDER}`]: asClass(provider).singleton(), + [`auth_provider_${provider.PROVIDER}`]: asClass( + provider as Constructor + ).singleton(), }) } diff --git a/packages/authentication/src/migrations/.snapshot-medusa-authentication.json b/packages/authentication/src/migrations/.snapshot-medusa-authentication.json index f7ebcf17c1ad9..c8b0c9c5ceb10 100644 --- a/packages/authentication/src/migrations/.snapshot-medusa-authentication.json +++ b/packages/authentication/src/migrations/.snapshot-medusa-authentication.json @@ -39,6 +39,15 @@ ], "mappedType": "enum" }, + "config": { + "name": "config", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "json" + }, "is_active": { "name": "is_active", "type": "boolean", diff --git a/packages/authentication/src/migrations/Migration20240115150119.ts b/packages/authentication/src/migrations/Migration20240115150119.ts new file mode 100644 index 0000000000000..44234347907b6 --- /dev/null +++ b/packages/authentication/src/migrations/Migration20240115150119.ts @@ -0,0 +1,13 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20240115150119 extends Migration { + + async up(): Promise { + this.addSql('alter table "auth_provider" add column "config" jsonb null;'); + } + + async down(): Promise { + this.addSql('alter table "auth_provider" drop column "config";'); + } + +} diff --git a/packages/authentication/src/models/auth-provider.ts b/packages/authentication/src/models/auth-provider.ts index 8febe9ff6f62f..204a7628266d3 100644 --- a/packages/authentication/src/models/auth-provider.ts +++ b/packages/authentication/src/models/auth-provider.ts @@ -5,6 +5,7 @@ import { PrimaryKey, Property, } from "@mikro-orm/core" + import { ProviderDomain } from "../types/repositories/auth-provider" type OptionalFields = "domain" | "is_active" @@ -22,6 +23,9 @@ export default class AuthProvider { @Enum({ items: () => ProviderDomain, default: ProviderDomain.ALL }) domain: ProviderDomain = ProviderDomain.ALL + @Property({ columnType: "jsonb", nullable: true }) + config: Record | null + @Property({ columnType: "boolean", default: false }) is_active = false } diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts new file mode 100644 index 0000000000000..4bd18995955f7 --- /dev/null +++ b/packages/authentication/src/providers/google.ts @@ -0,0 +1,273 @@ +import { + AbstractAuthenticationModuleProvider, + AuthenticationResponse, +} from "@medusajs/types" +import { AuthProviderService, AuthUserService } from "@services" + +import { OAuth2 } from "oauth" +import url, { UrlWithParsedQuery } from "url" +import jwt, { JwtPayload } from "jsonwebtoken" +import { MedusaError } from "@medusajs/utils" + +type InjectedDependencies = { + authUserService: AuthUserService + authProviderService: AuthProviderService +} + +type GoogleProviderConfig = { + callbackURL: string + clientID: string + clientSecret: string +} +const nullStore = { + store: function (req, cb) { + return cb() + }, + verify: function (req, state, cb) { + return cb(null, true) + }, +} + +class GoogleProvider extends AbstractAuthenticationModuleProvider { + public static PROVIDER = "google" + public static DISPLAY_NAME = "Google Authentication" + + private authorizationUrl_ = "https://accounts.google.com/o/oauth2/v2/auth" + private tokenUrl_ = "https://www.googleapis.com/oauth2/v4/token" + + protected readonly authUserSerivce_: AuthUserService + protected readonly authProviderService_: AuthProviderService + protected readonly _stateStore = nullStore + + constructor({ authUserService, authProviderService }: InjectedDependencies) { + super() + + this.authUserSerivce_ = authUserService + this.authProviderService_ = authProviderService + } + + private async validateConfig(config: Partial) { + if (!config.clientID) { + throw new Error("Google clientID is required") + } + + if (!config.clientSecret) { + throw new Error("Google clientSecret is required") + } + + if (!config.callbackURL) { + throw new Error("Google callbackUrl is required") + } + } + + async authenticate( + userData: Record + ): Promise { + const originalURL = (req) => { + var tls = req.connection.encrypted, + host = req.headers.host, + protocol = tls ? "https" : "http", + path = req.url || "" + return protocol + "://" + host + path + } + + const verify_ = async (request, accessToken, refreshToken, profile, done) => { + // decode email from jwt + const jwtData = await jwt.decode(refreshToken.id_token, { complete: true }) as JwtPayload | null + // const email = jwtData!.email + const entity_id = jwtData!.payload.email + + let authUser + + try { + authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( + entity_id, + GoogleProvider.PROVIDER + ) + } catch (error) { + if(error.type === MedusaError.Types.NOT_FOUND) { + authUser = await this.authUserSerivce_.create([{ + entity_id, + provider_id: GoogleProvider.PROVIDER, + user_metadata: { + email: jwtData!.payload.email, + email_verified: jwtData!.payload.email_verified, + name: jwtData!.payload.name, + picture: jwtData!.payload.picture, + locale: jwtData!.payload.locale, + family_name: jwtData!.payload.family_name, + given_name: jwtData!.payload.given_name, + hd: jwtData!.payload.hd, + }, + }]) + } + else { + return done(error.message) + } + } + + return done(null, authUser) + } + + const req = userData + + const provider = await this.authProviderService_.retrieve( + GoogleProvider.PROVIDER + ) + + try { + this.validateConfig(provider.config || {}) + } catch (error) { + return { success: false, error: error.message } + } + + let { callbackURL, clientID, clientSecret } = + provider.config as GoogleProviderConfig + + if (req.query && req.query.error) { + return { + success: false, + error: `${req.query.error_description}, read more at: ${req.query.error_uri}`, + } + } + + // var callbackURL = provider./callbackURL; + if (callbackURL) { + var parsed = url.parse(callbackURL) + if (!parsed.protocol) { + // The callback URL is relative, resolve a fully qualified URL from the + // URL of the originating request. + callbackURL = url.resolve(originalURL(req), callbackURL) + } + } + + var meta = { + authorizationURL: this.authorizationUrl_, + tokenURL: this.tokenUrl_, + clientID, + callbackURL: callbackURL, + } + + const oauth2 = new OAuth2( + clientID, + clientSecret, + "", + meta.authorizationURL, + meta.tokenURL + ) + + if ((req.query && req.query.code) || (req.body && req.body.code)) { + const stateStore = { + state: null, + + setState: function (state) { + this.state = state + }, + } + + function loaded(err, ok) { + if (err) { + stateStore.setState({ + success: false, + error: err, + }) + } + if (!ok) { + // return self.fail(state, 403); + // TODO: fail with status + return stateStore.setState({ + success: false, + error: "fail with status, 403", + }) + } + + var code = (req.query && req.query.code) || (req.body && req.body.code) + + var params: { + grant_type: string + redirect_uri?: string + code_verifier?: string + } = { grant_type: "authorization_code" } + if (callbackURL) { + params.redirect_uri = callbackURL + } + + oauth2.getOAuthAccessToken( + code, + params, + async (err, accessToken, refreshToken, params) => { + console.warn("testing") + if (err || !accessToken) { + return stateStore.setState({ + success: false, + error: "Failed to obtain access token", + }) + } + + function verified(err, user, info) { + if (err) { + stateStore.setState({ success: false, error: err }) + } else if (!user) { + stateStore.setState({ success: false, error: info }) + } else { + stateStore.setState({ success: true, authUser: user }) + } + } + + try { + await verify_(accessToken, refreshToken, params, {}, verified) + } catch (ex) { + stateStore.setState({ error: ex.message, success: false }) + } + } + ) + } + + var state = (req.query && req.query.state) || (req.body && req.body.state) + try { + this._stateStore.verify(req, state, loaded) + } catch (ex) { + return { success: false, error: ex } + } + + while(stateStore.state === null) { + await new Promise((resolve) => setTimeout(resolve, 100)) + } + + return stateStore.state + + } else { + var params: { + response_type: string + redirect_uri?: string + scope?: string + } = { response_type: "code", scope: "email profile" } + if (callbackURL) { + params.redirect_uri = callbackURL + } + + function stored(err, state) { + var parsed: Omit & { + search?: string | null + } = url.parse(oauth2._authorizeUrl, true) + for (const key in params) { + parsed.query[key] = params[key] + } + + parsed.query["client_id"] = meta.clientID + + delete parsed.search + var location = url.format(parsed) + return { success: true, location } // TODO: redirect + } + + try { + return this._stateStore.store(req, stored) + } catch (ex) { + return { success: false, error: ex } + } + } + } +} + +export default GoogleProvider diff --git a/packages/authentication/src/providers/index.ts b/packages/authentication/src/providers/index.ts index 77a2d444f782a..e00b4b172759d 100644 --- a/packages/authentication/src/providers/index.ts +++ b/packages/authentication/src/providers/index.ts @@ -1 +1,2 @@ export { default as UsernamePasswordProvider } from "./username-password" +export { default as GoogleProvider } from "./google" \ No newline at end of file diff --git a/packages/authentication/src/types/services/auth-provider.ts b/packages/authentication/src/types/services/auth-provider.ts index 8f1307ab07c29..d3419451c540c 100644 --- a/packages/authentication/src/types/services/auth-provider.ts +++ b/packages/authentication/src/types/services/auth-provider.ts @@ -10,6 +10,7 @@ export type CreateAuthProviderDTO = { name: string domain?: ProviderDomain is_active?: boolean + config?: Record } export type UpdateAuthProviderDTO = { diff --git a/packages/authentication/src/types/services/auth-user.ts b/packages/authentication/src/types/services/auth-user.ts index c059e980f8b9c..5e9898023fd8d 100644 --- a/packages/authentication/src/types/services/auth-user.ts +++ b/packages/authentication/src/types/services/auth-user.ts @@ -11,7 +11,7 @@ export type AuthUserDTO = { } export type CreateAuthUserDTO = { - entity_id: string + entity_id?: string provider_id: string provider_metadata?: Record user_metadata?: Record From 310b0e5296369fc62774599a97a9b63f35c44a2a Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 07:03:07 +0100 Subject: [PATCH 34/53] update lockfile --- yarn.lock | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/yarn.lock b/yarn.lock index dcfec06735e9f..b095cd87b9338 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7854,8 +7854,10 @@ __metadata: cross-env: ^5.2.1 dotenv: ^16.1.4 jest: ^29.6.3 + jsonwebtoken: ^9.0.2 knex: 2.4.2 medusa-test-utils: ^1.1.40 + oauth: ^0.10.0 rimraf: ^3.0.2 scrypt-kdf: ^2.0.1 ts-jest: ^29.1.1 @@ -35407,6 +35409,24 @@ __metadata: languageName: node linkType: hard +"jsonwebtoken@npm:^9.0.2": + version: 9.0.2 + resolution: "jsonwebtoken@npm:9.0.2" + dependencies: + jws: ^3.2.2 + lodash.includes: ^4.3.0 + lodash.isboolean: ^3.0.3 + lodash.isinteger: ^4.0.4 + lodash.isnumber: ^3.0.3 + lodash.isplainobject: ^4.0.6 + lodash.isstring: ^4.0.1 + lodash.once: ^4.0.0 + ms: ^2.1.1 + semver: ^7.5.4 + checksum: d287a29814895e866db2e5a0209ce730cbc158441a0e5a70d5e940eb0d28ab7498c6bf45029cc8b479639bca94056e9a7f254e2cdb92a2f5750c7f358657a131 + languageName: node + linkType: hard + "jsprim@npm:^1.2.2": version: 1.4.2 resolution: "jsprim@npm:1.4.2" @@ -39440,6 +39460,13 @@ __metadata: languageName: node linkType: hard +"oauth@npm:^0.10.0": + version: 0.10.0 + resolution: "oauth@npm:0.10.0" + checksum: 76f3e186cfd76cb33e5d5d442861c86680a5c3b71b2db1b854212087532c265a69de1a2ab9db683e6c6df733e17cfc67476527b81b224a19c1917de2bc3f75fa + languageName: node + linkType: hard + "obj-case@npm:0.2.1": version: 0.2.1 resolution: "obj-case@npm:0.2.1" From 433642b2c8b927e95de5238920b1746900144f0b Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 10:40:58 +0100 Subject: [PATCH 35/53] fix conflicts --- packages/authentication/src/types/services/auth-user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/authentication/src/types/services/auth-user.ts b/packages/authentication/src/types/services/auth-user.ts index 5e9898023fd8d..c059e980f8b9c 100644 --- a/packages/authentication/src/types/services/auth-user.ts +++ b/packages/authentication/src/types/services/auth-user.ts @@ -11,7 +11,7 @@ export type AuthUserDTO = { } export type CreateAuthUserDTO = { - entity_id?: string + entity_id: string provider_id: string provider_metadata?: Record user_metadata?: Record From 9010d3833c390ecef144f03b63c0c3812857a72c Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 10:43:28 +0100 Subject: [PATCH 36/53] add config variables --- packages/authentication/src/types/repositories/auth-provider.ts | 2 ++ packages/authentication/src/types/services/auth-provider.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/authentication/src/types/repositories/auth-provider.ts b/packages/authentication/src/types/repositories/auth-provider.ts index 31f5ce087a5f9..6f1ea0dc88e0f 100644 --- a/packages/authentication/src/types/repositories/auth-provider.ts +++ b/packages/authentication/src/types/repositories/auth-provider.ts @@ -5,6 +5,7 @@ export type CreateAuthProviderDTO = { name: string domain?: ProviderDomain is_active?: boolean + config?: Record } export type UpdateAuthProviderDTO = { @@ -13,6 +14,7 @@ export type UpdateAuthProviderDTO = { name?: string domain?: ProviderDomain is_active?: boolean + config?: Record } provider: AuthProvider } diff --git a/packages/authentication/src/types/services/auth-provider.ts b/packages/authentication/src/types/services/auth-provider.ts index d3419451c540c..8ef5d9b3b9bdd 100644 --- a/packages/authentication/src/types/services/auth-provider.ts +++ b/packages/authentication/src/types/services/auth-provider.ts @@ -3,6 +3,7 @@ export type AuthProviderDTO = { name: string domain: ProviderDomain is_active: boolean + config: Record } export type CreateAuthProviderDTO = { @@ -18,6 +19,7 @@ export type UpdateAuthProviderDTO = { name?: string domain?: ProviderDomain is_active?: boolean + config?: Record } export enum ProviderDomain { From 75a80fa4c60c95ac1f8ff9258beef984c4812687 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 10:50:51 +0100 Subject: [PATCH 37/53] update types --- packages/authentication/src/models/auth-provider.ts | 2 +- packages/authentication/src/providers/google.ts | 4 ++-- packages/authentication/src/services/authentication-module.ts | 4 ++-- packages/types/src/authentication/common/auth-provider.ts | 3 +++ 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/authentication/src/models/auth-provider.ts b/packages/authentication/src/models/auth-provider.ts index 204a7628266d3..29a0d241c5390 100644 --- a/packages/authentication/src/models/auth-provider.ts +++ b/packages/authentication/src/models/auth-provider.ts @@ -24,7 +24,7 @@ export default class AuthProvider { domain: ProviderDomain = ProviderDomain.ALL @Property({ columnType: "jsonb", nullable: true }) - config: Record | null + config: Record | null @Property({ columnType: "boolean", default: false }) is_active = false diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index 4bd18995955f7..1bb1080380157 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -225,7 +225,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { var state = (req.query && req.query.state) || (req.body && req.body.state) try { - this._stateStore.verify(req, state, loaded) + this._stateStore.verify(req, state, loaded) // TODO: call loaded } catch (ex) { return { success: false, error: ex } } @@ -246,7 +246,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { params.redirect_uri = callbackURL } - function stored(err, state) { + function stored() { var parsed: Omit & { search?: string | null } = url.parse(oauth2._authorizeUrl, true) diff --git a/packages/authentication/src/services/authentication-module.ts b/packages/authentication/src/services/authentication-module.ts index 93365943b3788..ddf4498b81700 100644 --- a/packages/authentication/src/services/authentication-module.ts +++ b/packages/authentication/src/services/authentication-module.ts @@ -158,7 +158,7 @@ export default class AuthenticationModuleService< protected async createAuthProviders_( data: any[], @MedusaContext() sharedContext: Context - ): Promise { + ): Promise { return await this.authProviderService_.create(data, sharedContext) } @@ -196,7 +196,7 @@ export default class AuthenticationModuleService< async updateAuthProvider_( data: AuthenticationTypes.UpdateAuthProviderDTO[], @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise { return await this.authProviderService_.update(data, sharedContext) } diff --git a/packages/types/src/authentication/common/auth-provider.ts b/packages/types/src/authentication/common/auth-provider.ts index f39d01b21755f..e3653642136f8 100644 --- a/packages/types/src/authentication/common/auth-provider.ts +++ b/packages/types/src/authentication/common/auth-provider.ts @@ -5,6 +5,7 @@ export type AuthProviderDTO = { name: string domain: ProviderDomain is_active: boolean + config: Record | null } export type CreateAuthProviderDTO = { @@ -12,6 +13,7 @@ export type CreateAuthProviderDTO = { name: string domain?: ProviderDomain is_active?: boolean + config?: Record } export type UpdateAuthProviderDTO = { @@ -19,6 +21,7 @@ export type UpdateAuthProviderDTO = { name?: string domain?: ProviderDomain is_active?: boolean + config?: Record } export enum ProviderDomain { From cc9f58ed427f81a32c96c340e81b53b248078937 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 14:00:23 +0100 Subject: [PATCH 38/53] refactor google provider --- .../authentication/src/providers/google.ts | 340 +++++++++--------- 1 file changed, 170 insertions(+), 170 deletions(-) diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index 1bb1080380157..b7f407ab1f217 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -8,6 +8,7 @@ import { OAuth2 } from "oauth" import url, { UrlWithParsedQuery } from "url" import jwt, { JwtPayload } from "jsonwebtoken" import { MedusaError } from "@medusajs/utils" +import { TreeLevelColumn } from "typeorm" type InjectedDependencies = { authUserService: AuthUserService @@ -19,14 +20,6 @@ type GoogleProviderConfig = { clientID: string clientSecret: string } -const nullStore = { - store: function (req, cb) { - return cb() - }, - verify: function (req, state, cb) { - return cb(null, true) - }, -} class GoogleProvider extends AbstractAuthenticationModuleProvider { public static PROVIDER = "google" @@ -37,7 +30,6 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { protected readonly authUserSerivce_: AuthUserService protected readonly authProviderService_: AuthProviderService - protected readonly _stateStore = nullStore constructor({ authUserService, authProviderService }: InjectedDependencies) { super() @@ -60,69 +52,68 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { } } - async authenticate( - userData: Record - ): Promise { - const originalURL = (req) => { - var tls = req.connection.encrypted, - host = req.headers.host, - protocol = tls ? "https" : "http", - path = req.url || "" - return protocol + "://" + host + path - } + private originalURL(req) { + var tls = req.connection.encrypted, + host = req.headers.host, + protocol = tls ? "https" : "http", + path = req.url || "" + return protocol + "://" + host + path + } + + async verify_(request, accessToken, refreshToken) { + // decode email from jwt + const jwtData = (await jwt.decode(refreshToken.id_token, { + complete: true, + })) as JwtPayload | null + // const email = jwtData!.email + const entity_id = jwtData!.payload.email + + let authUser - const verify_ = async (request, accessToken, refreshToken, profile, done) => { - // decode email from jwt - const jwtData = await jwt.decode(refreshToken.id_token, { complete: true }) as JwtPayload | null - // const email = jwtData!.email - const entity_id = jwtData!.payload.email - - let authUser - - try { - authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( - entity_id, - GoogleProvider.PROVIDER - ) - } catch (error) { - if(error.type === MedusaError.Types.NOT_FOUND) { - authUser = await this.authUserSerivce_.create([{ + try { + authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( + entity_id, + GoogleProvider.PROVIDER + ) + } catch (error) { + if (error.type === MedusaError.Types.NOT_FOUND) { + authUser = await this.authUserSerivce_.create([ + { entity_id, provider_id: GoogleProvider.PROVIDER, - user_metadata: { - email: jwtData!.payload.email, - email_verified: jwtData!.payload.email_verified, - name: jwtData!.payload.name, - picture: jwtData!.payload.picture, - locale: jwtData!.payload.locale, - family_name: jwtData!.payload.family_name, - given_name: jwtData!.payload.given_name, - hd: jwtData!.payload.hd, - }, - }]) - } - else { - return done(error.message) - } + user_metadata: jwtData!.payload, + }, + ]) + } else { + return { success: false, error: error.message } } - - return done(null, authUser) } - const req = userData + return { success: true, authUser } + } - const provider = await this.authProviderService_.retrieve( + async getProviderConfig( + req: Record + ): Promise { + const { config } = await this.authProviderService_.retrieve( GoogleProvider.PROVIDER ) - try { - this.validateConfig(provider.config || {}) - } catch (error) { - return { success: false, error: error.message } - } + this.validateConfig(config || {}) + + const { callbackURL } = config as GoogleProviderConfig + + const cbUrl = !url.parse(callbackURL).protocol + ? url.resolve(this.originalURL(req), callbackURL) + : callbackURL - let { callbackURL, clientID, clientSecret } = - provider.config as GoogleProviderConfig + return {...config, callbackURL: cbUrl } as GoogleProviderConfig + } + + async authenticate( + userData: Record + ): Promise { + const req = userData if (req.query && req.query.error) { return { @@ -131,141 +122,150 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { } } - // var callbackURL = provider./callbackURL; - if (callbackURL) { - var parsed = url.parse(callbackURL) - if (!parsed.protocol) { - // The callback URL is relative, resolve a fully qualified URL from the - // URL of the originating request. - callbackURL = url.resolve(originalURL(req), callbackURL) - } + let config + + try { + config = await this.getProviderConfig(req) + } catch (error) { + return { success: false, error: error.message } } + let { callbackURL, clientID, clientSecret } = config + var meta = { authorizationURL: this.authorizationUrl_, tokenURL: this.tokenUrl_, clientID, - callbackURL: callbackURL, + callbackURL, + clientSecret, + } + + const code = (req.query && req.query.code) || (req.body && req.body.code) + + // Redirect to google + if (!code) { + return this.getRedirect(meta) } + return await this.validateCallback(code, meta) + } + + // abstractable + private async validateCallback(code: string, { + authorizationURL, + tokenURL, + clientID, + callbackURL, + clientSecret, + }: { + authorizationURL: string + tokenURL: string + clientID: string + callbackURL: string + clientSecret: string + }) { const oauth2 = new OAuth2( clientID, clientSecret, "", - meta.authorizationURL, - meta.tokenURL + authorizationURL, + tokenURL ) - if ((req.query && req.query.code) || (req.body && req.body.code)) { - const stateStore = { - state: null, + let state = null + const setState = (newState) => { + state = newState + } + + try { + oauth2.getOAuthAccessToken( + code, + { grant_type: "authorization_code", redirect_uri: callbackURL }, + this.getOAuthAccessTokenCallback(setState) + ) + } catch (ex) { + return { success: false, error: ex } + } - setState: function (state) { - this.state = state - }, - } + // wait for callback to resolve + while (state === null) { + await new Promise((resolve) => setTimeout(resolve, 50)) + } - function loaded(err, ok) { - if (err) { - stateStore.setState({ - success: false, - error: err, - }) - } - if (!ok) { - // return self.fail(state, 403); - // TODO: fail with status - return stateStore.setState({ - success: false, - error: "fail with status, 403", - }) - } - - var code = (req.query && req.query.code) || (req.body && req.body.code) - - var params: { - grant_type: string - redirect_uri?: string - code_verifier?: string - } = { grant_type: "authorization_code" } - if (callbackURL) { - params.redirect_uri = callbackURL - } - - oauth2.getOAuthAccessToken( - code, - params, - async (err, accessToken, refreshToken, params) => { - console.warn("testing") - if (err || !accessToken) { - return stateStore.setState({ - success: false, - error: "Failed to obtain access token", - }) - } - - function verified(err, user, info) { - if (err) { - stateStore.setState({ success: false, error: err }) - } else if (!user) { - stateStore.setState({ success: false, error: info }) - } else { - stateStore.setState({ success: true, authUser: user }) - } - } - - try { - await verify_(accessToken, refreshToken, params, {}, verified) - } catch (ex) { - stateStore.setState({ error: ex.message, success: false }) - } - } - ) - } + return state + } - var state = (req.query && req.query.state) || (req.body && req.body.state) - try { - this._stateStore.verify(req, state, loaded) // TODO: call loaded - } catch (ex) { - return { success: false, error: ex } - } + private getOAuthAccessTokenCallback(setResult) { + return async (err, accessToken, refreshToken, params) => { + const result = await this.oathAccessTokenCallback( + err, + accessToken, + refreshToken, + params + ) + setResult(result) + } + } - while(stateStore.state === null) { - await new Promise((resolve) => setTimeout(resolve, 100)) + // abstractable + private async oathAccessTokenCallback( + err, + accessToken, + refreshToken, + params + ) { + if (err || !accessToken) { + return { + success: false, + error: "Failed to obtain access token", } + } + + try { + return await this.verify_(accessToken, refreshToken, params) + } catch (ex) { + return { error: ex.message, success: false } + } + } - return stateStore.state + // Abstractable + private getRedirect({ + authorizationURL, + clientID, + callbackURL + }: { + authorizationURL: string + clientID: string + callbackURL: string + }) { + const params: { + response_type: string + redirect_uri: string + scope?: string + } = { + response_type: "code", + scope: "email profile", + redirect_uri: callbackURL, + } - } else { - var params: { - response_type: string - redirect_uri?: string - scope?: string - } = { response_type: "code", scope: "email profile" } - if (callbackURL) { - params.redirect_uri = callbackURL + try { + const parsed: Omit & { + search?: string | null + } = url.parse(authorizationURL, true) + + for (const key in params) { + parsed.query[key] = params[key] } - function stored() { - var parsed: Omit & { - search?: string | null - } = url.parse(oauth2._authorizeUrl, true) - for (const key in params) { - parsed.query[key] = params[key] - } + parsed.query["client_id"] = clientID - parsed.query["client_id"] = meta.clientID + delete parsed.search - delete parsed.search - var location = url.format(parsed) - return { success: true, location } // TODO: redirect - } + var location = url.format(parsed) - try { - return this._stateStore.store(req, stored) - } catch (ex) { - return { success: false, error: ex } - } + return { success: true, location } + } catch (ex) { + return { success: false, error: ex.message } } } } From 1d9949c2bf4ca15a106a1d5e3f27d79cc5ddaa89 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 17 Jan 2024 14:01:15 +0100 Subject: [PATCH 39/53] re-order methods --- .../authentication/src/providers/google.ts | 66 ++++++++++--------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index b7f407ab1f217..10686b32b64df 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -60,38 +60,6 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { return protocol + "://" + host + path } - async verify_(request, accessToken, refreshToken) { - // decode email from jwt - const jwtData = (await jwt.decode(refreshToken.id_token, { - complete: true, - })) as JwtPayload | null - // const email = jwtData!.email - const entity_id = jwtData!.payload.email - - let authUser - - try { - authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( - entity_id, - GoogleProvider.PROVIDER - ) - } catch (error) { - if (error.type === MedusaError.Types.NOT_FOUND) { - authUser = await this.authUserSerivce_.create([ - { - entity_id, - provider_id: GoogleProvider.PROVIDER, - user_metadata: jwtData!.payload, - }, - ]) - } else { - return { success: false, error: error.message } - } - } - - return { success: true, authUser } - } - async getProviderConfig( req: Record ): Promise { @@ -195,6 +163,40 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { return state } + // abstractable + async verify_(request, accessToken, refreshToken) { + // decode email from jwt + const jwtData = (await jwt.decode(refreshToken.id_token, { + complete: true, + })) as JwtPayload | null + // const email = jwtData!.email + const entity_id = jwtData!.payload.email + + let authUser + + try { + authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( + entity_id, + GoogleProvider.PROVIDER + ) + } catch (error) { + if (error.type === MedusaError.Types.NOT_FOUND) { + authUser = await this.authUserSerivce_.create([ + { + entity_id, + provider_id: GoogleProvider.PROVIDER, + user_metadata: jwtData!.payload, + }, + ]) + } else { + return { success: false, error: error.message } + } + } + + return { success: true, authUser } + } + + private getOAuthAccessTokenCallback(setResult) { return async (err, accessToken, refreshToken, params) => { const result = await this.oathAccessTokenCallback( From c93a54a1ba67a8c96c7d5ff4aa756e327d5be9dd Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Mon, 22 Jan 2024 10:44:10 +0700 Subject: [PATCH 40/53] fix pr feedback p. 1 --- .../.snapshot-medusa-authentication.json | 2 +- .../src/migrations/Migration20240115092929.ts | 15 ----- .../src/migrations/Migration20240115150119.ts | 13 ----- ...04154451.ts => Migration20240122041959.ts} | 7 ++- .../src/models/auth-provider.ts | 4 +- .../authentication/src/providers/google.ts | 58 ++++++++++++------- 6 files changed, 44 insertions(+), 55 deletions(-) delete mode 100644 packages/authentication/src/migrations/Migration20240115092929.ts delete mode 100644 packages/authentication/src/migrations/Migration20240115150119.ts rename packages/authentication/src/migrations/{Migration20240104154451.ts => Migration20240122041959.ts} (56%) diff --git a/packages/authentication/src/migrations/.snapshot-medusa-authentication.json b/packages/authentication/src/migrations/.snapshot-medusa-authentication.json index c8b0c9c5ceb10..a31fe796e7234 100644 --- a/packages/authentication/src/migrations/.snapshot-medusa-authentication.json +++ b/packages/authentication/src/migrations/.snapshot-medusa-authentication.json @@ -45,7 +45,7 @@ "unsigned": false, "autoincrement": false, "primary": false, - "nullable": false, + "nullable": true, "mappedType": "json" }, "is_active": { diff --git a/packages/authentication/src/migrations/Migration20240115092929.ts b/packages/authentication/src/migrations/Migration20240115092929.ts deleted file mode 100644 index d06392934456e..0000000000000 --- a/packages/authentication/src/migrations/Migration20240115092929.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Migration } from '@mikro-orm/migrations'; - -export class Migration20240115092929 extends Migration { - - async up(): Promise { - this.addSql('alter table "auth_user" add column "entity_id" text not null;'); - this.addSql('alter table "auth_user" add constraint "IDX_auth_user_provider_entity_id" unique ("provider_id", "entity_id");'); - } - - async down(): Promise { - this.addSql('alter table "auth_user" drop constraint "IDX_auth_user_provider_entity_id";'); - this.addSql('alter table "auth_user" drop column "entity_id";'); - } - -} diff --git a/packages/authentication/src/migrations/Migration20240115150119.ts b/packages/authentication/src/migrations/Migration20240115150119.ts deleted file mode 100644 index 44234347907b6..0000000000000 --- a/packages/authentication/src/migrations/Migration20240115150119.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Migration } from '@mikro-orm/migrations'; - -export class Migration20240115150119 extends Migration { - - async up(): Promise { - this.addSql('alter table "auth_provider" add column "config" jsonb null;'); - } - - async down(): Promise { - this.addSql('alter table "auth_provider" drop column "config";'); - } - -} diff --git a/packages/authentication/src/migrations/Migration20240104154451.ts b/packages/authentication/src/migrations/Migration20240122041959.ts similarity index 56% rename from packages/authentication/src/migrations/Migration20240104154451.ts rename to packages/authentication/src/migrations/Migration20240122041959.ts index 2ffaf00de1668..e4bcc513f218d 100644 --- a/packages/authentication/src/migrations/Migration20240104154451.ts +++ b/packages/authentication/src/migrations/Migration20240122041959.ts @@ -1,11 +1,12 @@ import { Migration } from '@mikro-orm/migrations'; -export class Migration20240104154451 extends Migration { +export class Migration20240122041959 extends Migration { async up(): Promise { - this.addSql('create table "auth_provider" ("provider" text not null, "name" text not null, "domain" text check ("domain" in (\'all\', \'store\', \'admin\')) not null default \'all\', "is_active" boolean not null default false, constraint "auth_provider_pkey" primary key ("provider"));'); + this.addSql('create table "auth_provider" ("provider" text not null, "name" text not null, "domain" text check ("domain" in (\'all\', \'store\', \'admin\')) not null default \'all\', "config" jsonb null, "is_active" boolean not null default false, constraint "auth_provider_pkey" primary key ("provider"));'); - this.addSql('create table "auth_user" ("id" text not null, "provider_id" text null, "user_metadata" jsonb null, "app_metadata" jsonb null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));'); + this.addSql('create table "auth_user" ("id" text not null, "entity_id" text not null, "provider_id" text null, "user_metadata" jsonb null, "app_metadata" jsonb null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));'); + this.addSql('alter table "auth_user" add constraint "IDX_auth_user_provider_entity_id" unique ("provider_id", "entity_id");'); this.addSql('alter table "auth_user" add constraint "auth_user_provider_id_foreign" foreign key ("provider_id") references "auth_provider" ("provider") on delete cascade;'); } diff --git a/packages/authentication/src/models/auth-provider.ts b/packages/authentication/src/models/auth-provider.ts index 29a0d241c5390..0827186069bbd 100644 --- a/packages/authentication/src/models/auth-provider.ts +++ b/packages/authentication/src/models/auth-provider.ts @@ -8,7 +8,7 @@ import { import { ProviderDomain } from "../types/repositories/auth-provider" -type OptionalFields = "domain" | "is_active" +type OptionalFields = "domain" | "is_active" | "config" @Entity() export default class AuthProvider { @@ -24,7 +24,7 @@ export default class AuthProvider { domain: ProviderDomain = ProviderDomain.ALL @Property({ columnType: "jsonb", nullable: true }) - config: Record | null + config: Record | null = null @Property({ columnType: "boolean", default: false }) is_active = false diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index 10686b32b64df..4206a70375394 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -3,11 +3,12 @@ import { AuthenticationResponse, } from "@medusajs/types" import { AuthProviderService, AuthUserService } from "@services" - -import { OAuth2 } from "oauth" -import url, { UrlWithParsedQuery } from "url" import jwt, { JwtPayload } from "jsonwebtoken" +import url, { UrlWithParsedQuery } from "url" + +import { AuthProvider } from "@models" import { MedusaError } from "@medusajs/utils" +import { OAuth2 } from "oauth" import { TreeLevelColumn } from "typeorm" type InjectedDependencies = { @@ -25,8 +26,9 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { public static PROVIDER = "google" public static DISPLAY_NAME = "Google Authentication" - private authorizationUrl_ = "https://accounts.google.com/o/oauth2/v2/auth" - private tokenUrl_ = "https://www.googleapis.com/oauth2/v4/token" + // TODO: abstract + private readonly authorizationUrl_ = "https://accounts.google.com/o/oauth2/v2/auth" + private readonly tokenUrl_ = "https://www.googleapis.com/oauth2/v4/token" protected readonly authUserSerivce_: AuthUserService protected readonly authProviderService_: AuthProviderService @@ -53,7 +55,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { } private originalURL(req) { - var tls = req.connection.encrypted, + const tls = req.connection.encrypted, host = req.headers.host, protocol = tls ? "https" : "http", path = req.url || "" @@ -63,26 +65,24 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { async getProviderConfig( req: Record ): Promise { - const { config } = await this.authProviderService_.retrieve( + const { config } = (await this.authProviderService_.retrieve( GoogleProvider.PROVIDER - ) + )) as AuthProvider & { config: GoogleProviderConfig } this.validateConfig(config || {}) - const { callbackURL } = config as GoogleProviderConfig + const { callbackURL } = config - const cbUrl = !url.parse(callbackURL).protocol + const parsedCallbackUrl = !url.parse(callbackURL).protocol ? url.resolve(this.originalURL(req), callbackURL) : callbackURL - return {...config, callbackURL: cbUrl } as GoogleProviderConfig + return {...config, callbackURL: parsedCallbackUrl } } async authenticate( - userData: Record + req: Record ): Promise { - const req = userData - if (req.query && req.query.error) { return { success: false, @@ -100,7 +100,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { let { callbackURL, clientID, clientSecret } = config - var meta = { + const meta = { authorizationURL: this.authorizationUrl_, tokenURL: this.tokenUrl_, clientID, @@ -141,6 +141,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { ) let state = null + const setState = (newState) => { state = newState } @@ -151,6 +152,22 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { { grant_type: "authorization_code", redirect_uri: callbackURL }, this.getOAuthAccessTokenCallback(setState) ) + // await new Promise((resolve, reject) => { + // oauth2.getOAuthAccessToken( + // code, + // { grant_type: "authorization_code", redirect_uri: callbackURL }, + // (err, accessToken, refreshToken, params) => { + // return this.getOAuthAccessTokenCallback(setState)( + // err, + // accessToken, + // refreshToken, + // params + // ) + // .catch(reject) + // .finally(() => resolve()) + // } + // ) + // }) } catch (ex) { return { success: false, error: ex } } @@ -168,9 +185,8 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { // decode email from jwt const jwtData = (await jwt.decode(refreshToken.id_token, { complete: true, - })) as JwtPayload | null - // const email = jwtData!.email - const entity_id = jwtData!.payload.email + })) as JwtPayload + const entity_id = jwtData.payload.email let authUser @@ -199,7 +215,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { private getOAuthAccessTokenCallback(setResult) { return async (err, accessToken, refreshToken, params) => { - const result = await this.oathAccessTokenCallback( + const result = await this.oAuthAccessTokenCallback( err, accessToken, refreshToken, @@ -210,7 +226,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { } // abstractable - private async oathAccessTokenCallback( + private async oAuthAccessTokenCallback( err, accessToken, refreshToken, @@ -263,7 +279,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { delete parsed.search - var location = url.format(parsed) + const location = url.format(parsed) return { success: true, location } } catch (ex) { From ebe04762ad59df836369ea05e9c6476c4e82a81f Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Mon, 22 Jan 2024 17:22:22 +0800 Subject: [PATCH 41/53] add initial type and update callback authorization --- .../authentication/src/providers/google.ts | 165 ++++++++++-------- 1 file changed, 90 insertions(+), 75 deletions(-) diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index 4206a70375394..82b31ce741313 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -22,12 +22,21 @@ type GoogleProviderConfig = { clientSecret: string } +type AuthenticationInput = { + connection: { encrypted: boolean } + url: string + headers:{ host: string } + query: Record + body: Record +} + class GoogleProvider extends AbstractAuthenticationModuleProvider { public static PROVIDER = "google" public static DISPLAY_NAME = "Google Authentication" // TODO: abstract - private readonly authorizationUrl_ = "https://accounts.google.com/o/oauth2/v2/auth" + private readonly authorizationUrl_ = + "https://accounts.google.com/o/oauth2/v2/auth" private readonly tokenUrl_ = "https://www.googleapis.com/oauth2/v4/token" protected readonly authUserSerivce_: AuthUserService @@ -54,7 +63,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { } } - private originalURL(req) { + private originalURL(req: AuthenticationInput) { const tls = req.connection.encrypted, host = req.headers.host, protocol = tls ? "https" : "http", @@ -63,7 +72,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { } async getProviderConfig( - req: Record + req: AuthenticationInput ): Promise { const { config } = (await this.authProviderService_.retrieve( GoogleProvider.PROVIDER @@ -77,11 +86,11 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { ? url.resolve(this.originalURL(req), callbackURL) : callbackURL - return {...config, callbackURL: parsedCallbackUrl } + return { ...config, callbackURL: parsedCallbackUrl } } async authenticate( - req: Record + req: AuthenticationInput ): Promise { if (req.query && req.query.error) { return { @@ -119,19 +128,22 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { } // abstractable - private async validateCallback(code: string, { - authorizationURL, - tokenURL, - clientID, - callbackURL, - clientSecret, - }: { - authorizationURL: string - tokenURL: string - clientID: string - callbackURL: string - clientSecret: string - }) { + private async validateCallback( + code: string, + { + authorizationURL, + tokenURL, + clientID, + callbackURL, + clientSecret, + }: { + authorizationURL: string + tokenURL: string + clientID: string + callbackURL: string + clientSecret: string + } + ) { const oauth2 = new OAuth2( clientID, clientSecret, @@ -141,77 +153,80 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { ) let state = null - + const setState = (newState) => { state = newState } - + try { - oauth2.getOAuthAccessToken( - code, - { grant_type: "authorization_code", redirect_uri: callbackURL }, - this.getOAuthAccessTokenCallback(setState) - ) - // await new Promise((resolve, reject) => { - // oauth2.getOAuthAccessToken( - // code, - // { grant_type: "authorization_code", redirect_uri: callbackURL }, - // (err, accessToken, refreshToken, params) => { - // return this.getOAuthAccessTokenCallback(setState)( - // err, - // accessToken, - // refreshToken, - // params - // ) - // .catch(reject) - // .finally(() => resolve()) - // } - // ) - // }) + // oauth2.getOAuthAccessToken( + // code, + // { grant_type: "authorization_code", redirect_uri: callbackURL }, + // this.getOAuthAccessTokenCallback(setState) + // ) + await new Promise((resolve, reject) => { + oauth2.getOAuthAccessToken( + code, + { grant_type: "authorization_code", redirect_uri: callbackURL }, + (err, accessToken, refreshToken, params) => { + return this.getOAuthAccessTokenCallback(setState)( + err, + accessToken, + refreshToken, + params + ) + .catch(reject) + .finally(() => resolve()) + } + ) + }) } catch (ex) { return { success: false, error: ex } } - // wait for callback to resolve - while (state === null) { - await new Promise((resolve) => setTimeout(resolve, 50)) + if(!state) { + return { success: false, error: "Authentication failed"} } + // // wait for callback to resolve + // while (state === null) { + // await new Promise((resolve) => setTimeout(resolve, 50)) + // } + return state } - // abstractable - async verify_(request, accessToken, refreshToken) { - // decode email from jwt - const jwtData = (await jwt.decode(refreshToken.id_token, { - complete: true, - })) as JwtPayload - const entity_id = jwtData.payload.email - - let authUser - - try { - authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( - entity_id, - GoogleProvider.PROVIDER - ) - } catch (error) { - if (error.type === MedusaError.Types.NOT_FOUND) { - authUser = await this.authUserSerivce_.create([ - { - entity_id, - provider_id: GoogleProvider.PROVIDER, - user_metadata: jwtData!.payload, - }, - ]) - } else { - return { success: false, error: error.message } - } + // abstractable + async verify_(request, accessToken, refreshToken) { + // decode email from jwt + const jwtData = (await jwt.decode(refreshToken.id_token, { + complete: true, + })) as JwtPayload + const entity_id = jwtData.payload.email + + let authUser + + try { + authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( + entity_id, + GoogleProvider.PROVIDER + ) + } catch (error) { + if (error.type === MedusaError.Types.NOT_FOUND) { + authUser = await this.authUserSerivce_.create([ + { + entity_id, + provider_id: GoogleProvider.PROVIDER, + user_metadata: jwtData!.payload, + }, + ]) + } else { + return { success: false, error: error.message } } - - return { success: true, authUser } } - + + return { success: true, authUser } + } private getOAuthAccessTokenCallback(setResult) { return async (err, accessToken, refreshToken, params) => { @@ -250,7 +265,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { private getRedirect({ authorizationURL, clientID, - callbackURL + callbackURL, }: { authorizationURL: string clientID: string From ca53bc1b6788d31957796d464e2310221831af87 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Mon, 22 Jan 2024 18:36:34 +0800 Subject: [PATCH 42/53] add google provider to integration test --- .../services/module/providers.spec.ts | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/authentication/integration-tests/__tests__/services/module/providers.spec.ts b/packages/authentication/integration-tests/__tests__/services/module/providers.spec.ts index 3a49d6c3b0217..cefb712ce166e 100644 --- a/packages/authentication/integration-tests/__tests__/services/module/providers.spec.ts +++ b/packages/authentication/integration-tests/__tests__/services/module/providers.spec.ts @@ -1,11 +1,10 @@ -import { SqlEntityManager } from "@mikro-orm/postgresql" - -import { MikroOrmWrapper } from "../../../utils" -import { initialize } from "../../../../src" import { DB_URL } from "@medusajs/pricing/integration-tests/utils" -import { MedusaModule } from "@medusajs/modules-sdk" import { IAuthenticationModuleService } from "@medusajs/types" +import { MedusaModule } from "@medusajs/modules-sdk" +import { MikroOrmWrapper } from "../../../utils" +import { SqlEntityManager } from "@mikro-orm/postgresql" import { createAuthProviders } from "../../../__fixtures__/auth-provider" +import { initialize } from "../../../../src" jest.setTimeout(30000) @@ -24,7 +23,7 @@ describe("AuthenticationModuleService - AuthProvider", () => { }, }) - if(service.__hooks?.onApplicationStart) { + if (service.__hooks?.onApplicationStart) { await service.__hooks.onApplicationStart() } }) @@ -39,12 +38,18 @@ describe("AuthenticationModuleService - AuthProvider", () => { const authProviders = await service.listAuthProviders() const serialized = JSON.parse(JSON.stringify(authProviders)) - expect(serialized).toEqual([ - expect.objectContaining({ - provider: "usernamePassword", - name: "Username/Password Authentication", - }), - ]) + expect(serialized).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + provider: "usernamePassword", + name: "Username/Password Authentication", + }), + expect.objectContaining({ + provider: "google", + name: "Google Authentication", + }), + ]) + ) }) }) From 5427e72f751d8fa10570ba0116ee40c1ff2c595d Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Tue, 23 Jan 2024 14:52:46 +0800 Subject: [PATCH 43/53] fix feedback --- .../src/migrations/Migration20240122041959.ts | 6 +++--- packages/authentication/src/providers/google.ts | 10 ---------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/authentication/src/migrations/Migration20240122041959.ts b/packages/authentication/src/migrations/Migration20240122041959.ts index e4bcc513f218d..763930b944a9d 100644 --- a/packages/authentication/src/migrations/Migration20240122041959.ts +++ b/packages/authentication/src/migrations/Migration20240122041959.ts @@ -3,16 +3,16 @@ import { Migration } from '@mikro-orm/migrations'; export class Migration20240122041959 extends Migration { async up(): Promise { - this.addSql('create table "auth_provider" ("provider" text not null, "name" text not null, "domain" text check ("domain" in (\'all\', \'store\', \'admin\')) not null default \'all\', "config" jsonb null, "is_active" boolean not null default false, constraint "auth_provider_pkey" primary key ("provider"));'); + this.addSql('create table if not exists "auth_provider" ("provider" text not null, "name" text not null, "domain" text check ("domain" in (\'all\', \'store\', \'admin\')) not null default \'all\', "config" jsonb null, "is_active" boolean not null default false, constraint "auth_provider_pkey" primary key ("provider"));'); - this.addSql('create table "auth_user" ("id" text not null, "entity_id" text not null, "provider_id" text null, "user_metadata" jsonb null, "app_metadata" jsonb null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));'); + this.addSql('create table if not exists "auth_user" ("id" text not null, "entity_id" text not null, "provider_id" text null, "user_metadata" jsonb null, "app_metadata" jsonb null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));'); this.addSql('alter table "auth_user" add constraint "IDX_auth_user_provider_entity_id" unique ("provider_id", "entity_id");'); this.addSql('alter table "auth_user" add constraint "auth_user_provider_id_foreign" foreign key ("provider_id") references "auth_provider" ("provider") on delete cascade;'); } async down(): Promise { - this.addSql('alter table "auth_user" drop constraint "auth_user_provider_id_foreign";'); + this.addSql('alter table "auth_user" drop constraint if exists "auth_user_provider_id_foreign";'); this.addSql('drop table if exists "auth_provider" cascade;'); diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index 82b31ce741313..1e609f74b20bb 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -159,11 +159,6 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { } try { - // oauth2.getOAuthAccessToken( - // code, - // { grant_type: "authorization_code", redirect_uri: callbackURL }, - // this.getOAuthAccessTokenCallback(setState) - // ) await new Promise((resolve, reject) => { oauth2.getOAuthAccessToken( code, @@ -188,11 +183,6 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { return { success: false, error: "Authentication failed"} } - // // wait for callback to resolve - // while (state === null) { - // await new Promise((resolve) => setTimeout(resolve, 50)) - // } - return state } From 92563a3d7c8ec4493e56f6718bafa7a40fc6647a Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Tue, 23 Jan 2024 16:28:46 +0800 Subject: [PATCH 44/53] initial implementation (#6171) * initial implementation * remove oauth lib --- packages/authentication/package.json | 4 +- .../authentication/src/providers/google.ts | 187 +++++------------- yarn.lock | 81 ++++++-- 3 files changed, 122 insertions(+), 150 deletions(-) diff --git a/packages/authentication/package.json b/packages/authentication/package.json index 69e8356176e38..7fee6905fe378 100644 --- a/packages/authentication/package.json +++ b/packages/authentication/package.json @@ -58,7 +58,7 @@ "dotenv": "^16.1.4", "jsonwebtoken": "^9.0.2", "knex": "2.4.2", - "oauth": "^0.10.0", - "scrypt-kdf": "^2.0.1" + "scrypt-kdf": "^2.0.1", + "simple-oauth2": "^5.0.0" } } diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index 1e609f74b20bb..6842d042eaf6b 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -4,41 +4,35 @@ import { } from "@medusajs/types" import { AuthProviderService, AuthUserService } from "@services" import jwt, { JwtPayload } from "jsonwebtoken" -import url, { UrlWithParsedQuery } from "url" +import url from "url" import { AuthProvider } from "@models" import { MedusaError } from "@medusajs/utils" -import { OAuth2 } from "oauth" -import { TreeLevelColumn } from "typeorm" +import { AuthorizationCode } from "simple-oauth2" type InjectedDependencies = { authUserService: AuthUserService authProviderService: AuthProviderService } -type GoogleProviderConfig = { - callbackURL: string - clientID: string - clientSecret: string -} - type AuthenticationInput = { connection: { encrypted: boolean } url: string - headers:{ host: string } + headers: { host: string } query: Record body: Record } +type ProviderConfig = { + clientID: string + clientSecret: string + callbackURL: string +} + class GoogleProvider extends AbstractAuthenticationModuleProvider { public static PROVIDER = "google" public static DISPLAY_NAME = "Google Authentication" - // TODO: abstract - private readonly authorizationUrl_ = - "https://accounts.google.com/o/oauth2/v2/auth" - private readonly tokenUrl_ = "https://www.googleapis.com/oauth2/v4/token" - protected readonly authUserSerivce_: AuthUserService protected readonly authProviderService_: AuthProviderService @@ -49,7 +43,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { this.authProviderService_ = authProviderService } - private async validateConfig(config: Partial) { + private async validateConfig(config: Partial) { if (!config.clientID) { throw new Error("Google clientID is required") } @@ -71,12 +65,10 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { return protocol + "://" + host + path } - async getProviderConfig( - req: AuthenticationInput - ): Promise { + async getProviderConfig(req: AuthenticationInput): Promise { const { config } = (await this.authProviderService_.retrieve( GoogleProvider.PROVIDER - )) as AuthProvider & { config: GoogleProviderConfig } + )) as AuthProvider & { config: ProviderConfig } this.validateConfig(config || {}) @@ -109,9 +101,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { let { callbackURL, clientID, clientSecret } = config - const meta = { - authorizationURL: this.authorizationUrl_, - tokenURL: this.tokenUrl_, + const meta: ProviderConfig = { clientID, callbackURL, clientSecret, @@ -130,66 +120,27 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { // abstractable private async validateCallback( code: string, - { - authorizationURL, - tokenURL, - clientID, - callbackURL, - clientSecret, - }: { - authorizationURL: string - tokenURL: string - clientID: string - callbackURL: string - clientSecret: string - } + { clientID, callbackURL, clientSecret }: ProviderConfig ) { - const oauth2 = new OAuth2( - clientID, - clientSecret, - "", - authorizationURL, - tokenURL - ) - - let state = null + const client = this.getAuthorizationCodeHandler({ clientID, clientSecret }) - const setState = (newState) => { - state = newState + const tokenParams = { + code, + redirect_uri: callbackURL, } try { - await new Promise((resolve, reject) => { - oauth2.getOAuthAccessToken( - code, - { grant_type: "authorization_code", redirect_uri: callbackURL }, - (err, accessToken, refreshToken, params) => { - return this.getOAuthAccessTokenCallback(setState)( - err, - accessToken, - refreshToken, - params - ) - .catch(reject) - .finally(() => resolve()) - } - ) - }) - } catch (ex) { - return { success: false, error: ex } - } + const accessToken = await client.getToken(tokenParams) - if(!state) { - return { success: false, error: "Authentication failed"} + return await this.verify_(accessToken.id_token) + } catch (error) { + return { success: false, error: error.message } } - - return state } // abstractable - async verify_(request, accessToken, refreshToken) { - // decode email from jwt - const jwtData = (await jwt.decode(refreshToken.id_token, { + async verify_(refreshToken: string) { + const jwtData = (await jwt.decode(refreshToken, { complete: true, })) as JwtPayload const entity_id = jwtData.payload.email @@ -218,78 +169,40 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { return { success: true, authUser } } - private getOAuthAccessTokenCallback(setResult) { - return async (err, accessToken, refreshToken, params) => { - const result = await this.oAuthAccessTokenCallback( - err, - accessToken, - refreshToken, - params - ) - setResult(result) - } - } + // Abstractable + private getRedirect({ clientID, callbackURL, clientSecret }: ProviderConfig) { + const client = this.getAuthorizationCodeHandler({ clientID, clientSecret }) - // abstractable - private async oAuthAccessTokenCallback( - err, - accessToken, - refreshToken, - params - ) { - if (err || !accessToken) { - return { - success: false, - error: "Failed to obtain access token", - } - } + const location = client.authorizeURL({ + redirect_uri: callbackURL, + scope: "email profile", + }) - try { - return await this.verify_(accessToken, refreshToken, params) - } catch (ex) { - return { error: ex.message, success: false } - } + return { success: true, location } } - // Abstractable - private getRedirect({ - authorizationURL, + private getAuthorizationCodeHandler({ clientID, - callbackURL, + clientSecret, }: { - authorizationURL: string clientID: string - callbackURL: string + clientSecret: string }) { - const params: { - response_type: string - redirect_uri: string - scope?: string - } = { - response_type: "code", - scope: "email profile", - redirect_uri: callbackURL, - } - - try { - const parsed: Omit & { - search?: string | null - } = url.parse(authorizationURL, true) - - for (const key in params) { - parsed.query[key] = params[key] - } - - parsed.query["client_id"] = clientID - - delete parsed.search - - const location = url.format(parsed) - - return { success: true, location } - } catch (ex) { - return { success: false, error: ex.message } - } + const config = { + client: { + id: clientID, + secret: clientSecret, + }, + auth: { + // TODO: abstract to not be google specific + authorizeHost: "https://accounts.google.com", + authorizePath: "/o/oauth2/v2/auth", + tokenHost: "https://www.googleapis.com", + tokenPath: "/oauth2/v4/token", + }, + } + + return new AuthorizationCode(config) } } diff --git a/yarn.lock b/yarn.lock index b095cd87b9338..98b24d02b9cb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6060,14 +6060,44 @@ __metadata: languageName: node linkType: hard -"@hapi/hoek@npm:^9.0.0": +"@hapi/boom@npm:^10.0.1": + version: 10.0.1 + resolution: "@hapi/boom@npm:10.0.1" + dependencies: + "@hapi/hoek": ^11.0.2 + checksum: e4ae8a69bb67c5687320d320a0706ac66e797a659c19fb1c9b909eaefe3b41780e4ecd4382de1297b10c33e9db81f79667324576b9153f57b0cf701293b908d0 + languageName: node + linkType: hard + +"@hapi/bourne@npm:^3.0.0": + version: 3.0.0 + resolution: "@hapi/bourne@npm:3.0.0" + checksum: 2e2df62f6bc6f32b980ba5bbdc09200c93c55c8306399ec0f2781da088a82aab699498c89fe94fec4acf770210f9aee28c75bfc2f04044849ac01b034134e717 + languageName: node + linkType: hard + +"@hapi/hoek@npm:^10.0.1": + version: 10.0.1 + resolution: "@hapi/hoek@npm:10.0.1" + checksum: 320d5dc7a4070fa29e6344a3af9e44854980c6606848f7b7f59715174880cc09a1fe1e8adf44cf887100bd8d6a8664e9dc415986b30dc91df13455f7114de549 + languageName: node + linkType: hard + +"@hapi/hoek@npm:^11.0.2": + version: 11.0.4 + resolution: "@hapi/hoek@npm:11.0.4" + checksum: 3c0e487824daaf3af4c29e46fd57b0c5801ce9164fef2417c70e271cd970e13cc542b196f70ba1cfc9ef944eed825fcac261085ab5e2928c6017428bf576b363 + languageName: node + linkType: hard + +"@hapi/hoek@npm:^9.0.0, @hapi/hoek@npm:^9.3.0": version: 9.3.0 resolution: "@hapi/hoek@npm:9.3.0" checksum: a096063805051fb8bba4c947e293c664b05a32b47e13bc654c0dd43813a1cec993bdd8f29ceb838020299e1d0f89f68dc0d62a603c13c9cc8541963f0beca055 languageName: node linkType: hard -"@hapi/topo@npm:^5.0.0": +"@hapi/topo@npm:^5.0.0, @hapi/topo@npm:^5.1.0": version: 5.1.0 resolution: "@hapi/topo@npm:5.1.0" dependencies: @@ -6076,6 +6106,17 @@ __metadata: languageName: node linkType: hard +"@hapi/wreck@npm:^18.0.0": + version: 18.0.1 + resolution: "@hapi/wreck@npm:18.0.1" + dependencies: + "@hapi/boom": ^10.0.1 + "@hapi/bourne": ^3.0.0 + "@hapi/hoek": ^11.0.2 + checksum: 46b1b1f750a66c4724964eb6d9192d1d19cfa45e602386aae76f52e3b423c9ae14a03a0f0e9f962e7d973708e1b0b6ab42d2ae77539a691fa77a18c78ccf285c + languageName: node + linkType: hard + "@headlessui/react@npm:^1.7.18": version: 1.7.18 resolution: "@headlessui/react@npm:1.7.18" @@ -7857,9 +7898,9 @@ __metadata: jsonwebtoken: ^9.0.2 knex: 2.4.2 medusa-test-utils: ^1.1.40 - oauth: ^0.10.0 rimraf: ^3.0.2 scrypt-kdf: ^2.0.1 + simple-oauth2: ^5.0.0 ts-jest: ^29.1.1 ts-node: ^10.9.1 tsc-alias: ^1.8.6 @@ -12140,7 +12181,7 @@ __metadata: languageName: node linkType: hard -"@sideway/address@npm:^4.1.3": +"@sideway/address@npm:^4.1.3, @sideway/address@npm:^4.1.4": version: 4.1.4 resolution: "@sideway/address@npm:4.1.4" dependencies: @@ -34940,6 +34981,19 @@ __metadata: languageName: node linkType: hard +"joi@npm:^17.6.4": + version: 17.12.0 + resolution: "joi@npm:17.12.0" + dependencies: + "@hapi/hoek": ^9.3.0 + "@hapi/topo": ^5.1.0 + "@sideway/address": ^4.1.4 + "@sideway/formula": ^3.0.1 + "@sideway/pinpoint": ^2.0.0 + checksum: 2378f4ec8de2bc12674ce3e6faac509f52ff4f734c67bf68c288816b20336d4e59433ea1c1e187f1009075c81ec5fa8b5061094feb37a855d6e3ee0cfcd79dd8 + languageName: node + linkType: hard + "join-component@npm:^1.1.0": version: 1.1.0 resolution: "join-component@npm:1.1.0" @@ -39460,13 +39514,6 @@ __metadata: languageName: node linkType: hard -"oauth@npm:^0.10.0": - version: 0.10.0 - resolution: "oauth@npm:0.10.0" - checksum: 76f3e186cfd76cb33e5d5d442861c86680a5c3b71b2db1b854212087532c265a69de1a2ab9db683e6c6df733e17cfc67476527b81b224a19c1917de2bc3f75fa - languageName: node - linkType: hard - "obj-case@npm:0.2.1": version: 0.2.1 resolution: "obj-case@npm:0.2.1" @@ -46018,6 +46065,18 @@ __metadata: languageName: node linkType: hard +"simple-oauth2@npm:^5.0.0": + version: 5.0.0 + resolution: "simple-oauth2@npm:5.0.0" + dependencies: + "@hapi/hoek": ^10.0.1 + "@hapi/wreck": ^18.0.0 + debug: ^4.3.4 + joi: ^17.6.4 + checksum: 1cb5a4eb9022f656e1bb9a1f43d771dd058d4a4fa181b42d0e1e7ca7b5cfc42e35fad1c722be9bb6fa218398b3b0499010554a7367d2bd85eb9d7634f92546c1 + languageName: node + linkType: hard + "simple-string-table@npm:^1.0.0": version: 1.0.0 resolution: "simple-string-table@npm:1.0.0" From 6c4d0cf43fcbe1cf69da0b548af57fa1c6d9fa0b Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Tue, 23 Jan 2024 16:35:42 +0800 Subject: [PATCH 45/53] move abstract authentication provider --- packages/types/src/authentication/index.ts | 1 - .../authentication/abstract-authentication-provider.ts} | 7 ++++--- packages/utils/src/authentication/index.ts | 1 + packages/utils/src/index.ts | 1 + 4 files changed, 6 insertions(+), 4 deletions(-) rename packages/{types/src/authentication/provider.ts => utils/src/authentication/abstract-authentication-provider.ts} (76%) create mode 100644 packages/utils/src/authentication/index.ts diff --git a/packages/types/src/authentication/index.ts b/packages/types/src/authentication/index.ts index 711715ff6f885..1aa665fd541fe 100644 --- a/packages/types/src/authentication/index.ts +++ b/packages/types/src/authentication/index.ts @@ -1,3 +1,2 @@ export * from "./service" export * from "./common" -export * from "./provider" diff --git a/packages/types/src/authentication/provider.ts b/packages/utils/src/authentication/abstract-authentication-provider.ts similarity index 76% rename from packages/types/src/authentication/provider.ts rename to packages/utils/src/authentication/abstract-authentication-provider.ts index 8a709a6db93cc..52599feff85d1 100644 --- a/packages/types/src/authentication/provider.ts +++ b/packages/utils/src/authentication/abstract-authentication-provider.ts @@ -1,15 +1,16 @@ -import { AuthUserDTO } from "./common" +import { AuthUserDTO } from "@medusajs/types" export abstract class AbstractAuthenticationModuleProvider { public static PROVIDER: string public static DISPLAY_NAME: string public get provider() { - return (this.constructor as Function & { PROVIDER: string}).PROVIDER + return (this.constructor as Function & { PROVIDER: string }).PROVIDER } public get displayName() { - return (this.constructor as Function & { DISPLAY_NAME: string}).DISPLAY_NAME + return (this.constructor as Function & { DISPLAY_NAME: string }) + .DISPLAY_NAME } abstract authenticate( diff --git a/packages/utils/src/authentication/index.ts b/packages/utils/src/authentication/index.ts new file mode 100644 index 0000000000000..43c855ec7cf50 --- /dev/null +++ b/packages/utils/src/authentication/index.ts @@ -0,0 +1 @@ +export * from "./abstract-authentication-provider" diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 5f96dcb3dd3f6..d86097b681024 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,3 +1,4 @@ +export * from "./authentication" export * from "./bundles" export * from "./common" export * from "./dal" From eb7cb1ad842ff8a2833429da35e136b45ffd7407 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Tue, 23 Jan 2024 16:51:40 +0800 Subject: [PATCH 46/53] shuffle files around --- packages/authentication/src/providers/google.ts | 10 +++++----- .../authentication/src/providers/username-password.ts | 7 ++----- .../src/services/authentication-module.ts | 7 +++---- packages/types/src/authentication/common/index.ts | 1 + packages/types/src/authentication/common/provider.ts | 5 +++++ packages/types/src/authentication/service.ts | 7 ++++--- .../authentication/abstract-authentication-provider.ts | 8 +------- 7 files changed, 21 insertions(+), 24 deletions(-) create mode 100644 packages/types/src/authentication/common/provider.ts diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index 6842d042eaf6b..a78c9ef74d010 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -1,13 +1,13 @@ -import { - AbstractAuthenticationModuleProvider, - AuthenticationResponse, -} from "@medusajs/types" +import { AuthenticationResponse } from "@medusajs/types" import { AuthProviderService, AuthUserService } from "@services" import jwt, { JwtPayload } from "jsonwebtoken" import url from "url" import { AuthProvider } from "@models" -import { MedusaError } from "@medusajs/utils" +import { + AbstractAuthenticationModuleProvider, + MedusaError, +} from "@medusajs/utils" import { AuthorizationCode } from "simple-oauth2" type InjectedDependencies = { diff --git a/packages/authentication/src/providers/username-password.ts b/packages/authentication/src/providers/username-password.ts index c6428253dc311..b19f26c5d7526 100644 --- a/packages/authentication/src/providers/username-password.ts +++ b/packages/authentication/src/providers/username-password.ts @@ -1,11 +1,8 @@ -import { - AbstractAuthenticationModuleProvider, - AuthenticationResponse, -} from "@medusajs/types" +import { AuthenticationResponse } from "@medusajs/types" import { AuthUserService } from "@services" import Scrypt from "scrypt-kdf" -import { isString } from "@medusajs/utils" +import { AbstractAuthenticationModuleProvider, isString } from "@medusajs/utils" class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { public static PROVIDER = "usernamePassword" diff --git a/packages/authentication/src/services/authentication-module.ts b/packages/authentication/src/services/authentication-module.ts index ddf4498b81700..f2fdf4948cd31 100644 --- a/packages/authentication/src/services/authentication-module.ts +++ b/packages/authentication/src/services/authentication-module.ts @@ -1,5 +1,4 @@ import { - AbstractAuthenticationModuleProvider, AuthenticationResponse, AuthenticationTypes, Context, @@ -16,6 +15,7 @@ import { joinerConfig } from "../joiner-config" import { AuthProviderService, AuthUserService } from "@services" import { + AbstractAuthenticationModuleProvider, InjectManager, InjectTransactionManager, MedusaContext, @@ -380,15 +380,14 @@ export default class AuthenticationModuleService< await this.retrieveAuthProvider(provider, {}) registeredProvider = this.getRegisteredAuthenticationProvider(provider) - + return await registeredProvider.authenticate(authenticationData) } catch (error) { return { success: false, error: error.message } } } - - private async createProvidersOnLoad() { + private async createProvidersOnLoad() { const providersToLoad = this.__container__["auth_providers"] const providers = await this.authProviderService_.list({ diff --git a/packages/types/src/authentication/common/index.ts b/packages/types/src/authentication/common/index.ts index b4282c985c6a3..332e78fe42975 100644 --- a/packages/types/src/authentication/common/index.ts +++ b/packages/types/src/authentication/common/index.ts @@ -1,2 +1,3 @@ export * from "./auth-user" export * from "./auth-provider" +export * from "./provider" diff --git a/packages/types/src/authentication/common/provider.ts b/packages/types/src/authentication/common/provider.ts new file mode 100644 index 0000000000000..aef4339630a86 --- /dev/null +++ b/packages/types/src/authentication/common/provider.ts @@ -0,0 +1,5 @@ +export type AuthenticationResponse = { + success: boolean + authUser?: any + error?: string +} diff --git a/packages/types/src/authentication/service.ts b/packages/types/src/authentication/service.ts index 64d63fb25e574..ddd0d76c6b4a6 100644 --- a/packages/types/src/authentication/service.ts +++ b/packages/types/src/authentication/service.ts @@ -1,5 +1,5 @@ -import { IModuleService } from "../modules-sdk" import { + AuthenticationResponse, AuthProviderDTO, AuthUserDTO, CreateAuthProviderDTO, @@ -9,9 +9,10 @@ import { UpdateAuthProviderDTO, UpdateAuthUserDTO, } from "./common" -import { FindConfig } from "../common" + import { Context } from "../shared-context" -import { AuthenticationResponse } from "./provider" +import { FindConfig } from "../common" +import { IModuleService } from "../modules-sdk" export interface IAuthenticationModuleService extends IModuleService { authenticate( diff --git a/packages/utils/src/authentication/abstract-authentication-provider.ts b/packages/utils/src/authentication/abstract-authentication-provider.ts index 52599feff85d1..f4d69ab1bd82d 100644 --- a/packages/utils/src/authentication/abstract-authentication-provider.ts +++ b/packages/utils/src/authentication/abstract-authentication-provider.ts @@ -1,4 +1,4 @@ -import { AuthUserDTO } from "@medusajs/types" +import { AuthenticationResponse } from "@medusajs/types"; export abstract class AbstractAuthenticationModuleProvider { public static PROVIDER: string @@ -17,9 +17,3 @@ export abstract class AbstractAuthenticationModuleProvider { data: Record ): Promise } - -export type AuthenticationResponse = { - success: boolean - authUser?: AuthUserDTO - error?: string -} From 758c6757e924d32cf3e6461232fdaa6373c7630c Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Tue, 23 Jan 2024 20:55:58 +0800 Subject: [PATCH 47/53] Update packages/authentication/src/migrations/Migration20240122041959.ts Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com> --- .../authentication/src/migrations/Migration20240122041959.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/authentication/src/migrations/Migration20240122041959.ts b/packages/authentication/src/migrations/Migration20240122041959.ts index 763930b944a9d..d23f27a10750e 100644 --- a/packages/authentication/src/migrations/Migration20240122041959.ts +++ b/packages/authentication/src/migrations/Migration20240122041959.ts @@ -8,7 +8,7 @@ export class Migration20240122041959 extends Migration { this.addSql('create table if not exists "auth_user" ("id" text not null, "entity_id" text not null, "provider_id" text null, "user_metadata" jsonb null, "app_metadata" jsonb null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));'); this.addSql('alter table "auth_user" add constraint "IDX_auth_user_provider_entity_id" unique ("provider_id", "entity_id");'); - this.addSql('alter table "auth_user" add constraint "auth_user_provider_id_foreign" foreign key ("provider_id") references "auth_provider" ("provider") on delete cascade;'); + this.addSql('alter table "auth_user" add constraint if not exists "auth_user_provider_id_foreign" foreign key ("provider_id") references "auth_provider" ("provider") on delete cascade;'); } async down(): Promise { From 0b11ef5d9c7ccd9afca02dc1c39a75ed2035f8a9 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Tue, 23 Jan 2024 20:56:54 +0800 Subject: [PATCH 48/53] split authentication methods --- .../authentication/src/providers/google.ts | 159 ++++++++++-------- .../src/providers/username-password.ts | 2 +- .../src/services/authentication-module.ts | 32 ++++ packages/types/src/authentication/service.ts | 7 +- .../abstract-authentication-provider.ts | 12 +- 5 files changed, 134 insertions(+), 78 deletions(-) diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index a78c9ef74d010..dc4b1b830fd84 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -1,14 +1,14 @@ -import { AuthenticationResponse } from "@medusajs/types" -import { AuthProviderService, AuthUserService } from "@services" -import jwt, { JwtPayload } from "jsonwebtoken" -import url from "url" - -import { AuthProvider } from "@models" import { AbstractAuthenticationModuleProvider, MedusaError, } from "@medusajs/utils" +import { AuthProviderService, AuthUserService } from "@services" +import jwt, { JwtPayload } from "jsonwebtoken" + +import { AuthProvider } from "@models" +import { AuthenticationResponse } from "@medusajs/types" import { AuthorizationCode } from "simple-oauth2" +import url from "url" type InjectedDependencies = { authUserService: AuthUserService @@ -43,45 +43,28 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { this.authProviderService_ = authProviderService } - private async validateConfig(config: Partial) { - if (!config.clientID) { - throw new Error("Google clientID is required") + async initiateAuthentication( + req: AuthenticationInput + ): Promise { + if (req.query?.error) { + return { + success: false, + error: `${req.query.error_description}, read more at: ${req.query.error_uri}`, + } } - if (!config.clientSecret) { - throw new Error("Google clientSecret is required") - } + let config: ProviderConfig - if (!config.callbackURL) { - throw new Error("Google callbackUrl is required") + try { + config = await this.getProviderConfig(req) + } catch (error) { + return { success: false, error: error.message } } - } - - private originalURL(req: AuthenticationInput) { - const tls = req.connection.encrypted, - host = req.headers.host, - protocol = tls ? "https" : "http", - path = req.url || "" - return protocol + "://" + host + path - } - - async getProviderConfig(req: AuthenticationInput): Promise { - const { config } = (await this.authProviderService_.retrieve( - GoogleProvider.PROVIDER - )) as AuthProvider & { config: ProviderConfig } - - this.validateConfig(config || {}) - - const { callbackURL } = config - - const parsedCallbackUrl = !url.parse(callbackURL).protocol - ? url.resolve(this.originalURL(req), callbackURL) - : callbackURL - return { ...config, callbackURL: parsedCallbackUrl } + return this.getRedirect(config) } - async authenticate( + async authenticateCallback( req: AuthenticationInput ): Promise { if (req.query && req.query.error) { @@ -91,7 +74,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { } } - let config + let config: ProviderConfig try { config = await this.getProviderConfig(req) @@ -99,43 +82,9 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { return { success: false, error: error.message } } - let { callbackURL, clientID, clientSecret } = config - - const meta: ProviderConfig = { - clientID, - callbackURL, - clientSecret, - } - const code = (req.query && req.query.code) || (req.body && req.body.code) - // Redirect to google - if (!code) { - return this.getRedirect(meta) - } - - return await this.validateCallback(code, meta) - } - - // abstractable - private async validateCallback( - code: string, - { clientID, callbackURL, clientSecret }: ProviderConfig - ) { - const client = this.getAuthorizationCodeHandler({ clientID, clientSecret }) - - const tokenParams = { - code, - redirect_uri: callbackURL, - } - - try { - const accessToken = await client.getToken(tokenParams) - - return await this.verify_(accessToken.id_token) - } catch (error) { - return { success: false, error: error.message } - } + return await this.validateCallback(code, config) } // abstractable @@ -143,6 +92,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { const jwtData = (await jwt.decode(refreshToken, { complete: true, })) as JwtPayload + const entity_id = jwtData.payload.email let authUser @@ -169,6 +119,67 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { return { success: true, authUser } } + // abstractable + private async validateCallback( + code: string, + { clientID, callbackURL, clientSecret }: ProviderConfig + ) { + const client = this.getAuthorizationCodeHandler({ clientID, clientSecret }) + + const tokenParams = { + code, + redirect_uri: callbackURL, + } + + try { + const accessToken = await client.getToken(tokenParams) + + return await this.verify_(accessToken.token.id_token) + } catch (error) { + return { success: false, error: error.message } + } + } + + private async validateConfig(config: Partial) { + if (!config.clientID) { + throw new Error("Google clientID is required") + } + + if (!config.clientSecret) { + throw new Error("Google clientSecret is required") + } + + if (!config.callbackURL) { + throw new Error("Google callbackUrl is required") + } + } + + private originalURL(req: AuthenticationInput) { + const tls = req.connection.encrypted, + host = req.headers.host, + protocol = tls ? "https" : "http", + path = req.url || "" + return protocol + "://" + host + path + } + + private async getProviderConfig( + req: AuthenticationInput + ): Promise { + const { config } = (await this.authProviderService_.retrieve( + GoogleProvider.PROVIDER + )) as AuthProvider & { config: ProviderConfig } + + this.validateConfig(config || {}) + + const { callbackURL } = config + + const parsedCallbackUrl = !url.parse(callbackURL).protocol + ? url.resolve(this.originalURL(req), callbackURL) + : callbackURL + + return { ...config, callbackURL: parsedCallbackUrl } + } + // Abstractable private getRedirect({ clientID, callbackURL, clientSecret }: ProviderConfig) { const client = this.getAuthorizationCodeHandler({ clientID, clientSecret }) diff --git a/packages/authentication/src/providers/username-password.ts b/packages/authentication/src/providers/username-password.ts index b19f26c5d7526..851704652cc16 100644 --- a/packages/authentication/src/providers/username-password.ts +++ b/packages/authentication/src/providers/username-password.ts @@ -16,7 +16,7 @@ class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { this.authUserSerivce_ = AuthUserService } - async authenticate( + async initiateAuthentication( userData: Record ): Promise { const { email, password } = userData.body diff --git a/packages/authentication/src/services/authentication-module.ts b/packages/authentication/src/services/authentication-module.ts index f2fdf4948cd31..7b086f666c8a8 100644 --- a/packages/authentication/src/services/authentication-module.ts +++ b/packages/authentication/src/services/authentication-module.ts @@ -387,6 +387,38 @@ export default class AuthenticationModuleService< } } + async authenticateCallback( + provider: string, + authenticationData: Record + ): Promise { + try { + await this.retrieveAuthProvider(provider, {}) + + const registeredProvider = + this.getRegisteredAuthenticationProvider(provider) + + return await registeredProvider.authenticateCallback(authenticationData) + } catch (error) { + return { success: false, error: error.message } + } + } + + async initializeAuthentication( + provider: string, + authenticationData: Record + ): Promise { + try { + await this.retrieveAuthProvider(provider, {}) + + const registeredProvider = + this.getRegisteredAuthenticationProvider(provider) + + return await registeredProvider.initiateAuthentication(authenticationData) + } catch (error) { + return { success: false, error: error.message } + } + } + private async createProvidersOnLoad() { const providersToLoad = this.__container__["auth_providers"] diff --git a/packages/types/src/authentication/service.ts b/packages/types/src/authentication/service.ts index ddd0d76c6b4a6..144f5b929a24c 100644 --- a/packages/types/src/authentication/service.ts +++ b/packages/types/src/authentication/service.ts @@ -15,7 +15,12 @@ import { FindConfig } from "../common" import { IModuleService } from "../modules-sdk" export interface IAuthenticationModuleService extends IModuleService { - authenticate( + initializeAuthentication( + provider: string, + providerData: Record + ): Promise + + authenticateCallback( provider: string, providerData: Record ): Promise diff --git a/packages/utils/src/authentication/abstract-authentication-provider.ts b/packages/utils/src/authentication/abstract-authentication-provider.ts index f4d69ab1bd82d..128b7a6536117 100644 --- a/packages/utils/src/authentication/abstract-authentication-provider.ts +++ b/packages/utils/src/authentication/abstract-authentication-provider.ts @@ -1,4 +1,4 @@ -import { AuthenticationResponse } from "@medusajs/types"; +import { AuthenticationResponse } from "@medusajs/types" export abstract class AbstractAuthenticationModuleProvider { public static PROVIDER: string @@ -13,7 +13,15 @@ export abstract class AbstractAuthenticationModuleProvider { .DISPLAY_NAME } - abstract authenticate( + abstract initiateAuthentication( data: Record ): Promise + + public authenticateCallback( + data: Record + ): Promise { + throw new Error( + `Callback authentication not implemented for provider ${this.provider}` + ) + } } From 742adffa20c13c9722d0ad3ae70678c1b463459f Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Tue, 23 Jan 2024 20:57:38 +0800 Subject: [PATCH 49/53] call verify with token --- packages/authentication/src/providers/google.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index a78c9ef74d010..39bc0dbe9f6a8 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -1,14 +1,14 @@ -import { AuthenticationResponse } from "@medusajs/types" -import { AuthProviderService, AuthUserService } from "@services" -import jwt, { JwtPayload } from "jsonwebtoken" -import url from "url" - -import { AuthProvider } from "@models" import { AbstractAuthenticationModuleProvider, MedusaError, } from "@medusajs/utils" +import { AuthProviderService, AuthUserService } from "@services" +import jwt, { JwtPayload } from "jsonwebtoken" + +import { AuthProvider } from "@models" +import { AuthenticationResponse } from "@medusajs/types" import { AuthorizationCode } from "simple-oauth2" +import url from "url" type InjectedDependencies = { authUserService: AuthUserService @@ -132,7 +132,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { try { const accessToken = await client.getToken(tokenParams) - return await this.verify_(accessToken.id_token) + return await this.verify_(accessToken.token.id_token) } catch (error) { return { success: false, error: error.message } } From 072bf9b07a4d7250d1f651e46c0c072c0c3a6c6a Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Tue, 23 Jan 2024 21:38:42 +0800 Subject: [PATCH 50/53] update integration tests --- .../services/providers/username-password.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts b/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts index b8556ea1aa572..be493bc57c289 100644 --- a/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts +++ b/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts @@ -59,7 +59,7 @@ describe("AuthenticationModuleService - AuthProvider", () => { }, ]) - const res = await service.authenticate("usernamePassword", { + const res = await service.initializeAuthentication("usernamePassword", { body: { email: "test@test.com", password: password, @@ -81,7 +81,7 @@ describe("AuthenticationModuleService - AuthProvider", () => { await seedDefaultData(testManager) - const res = await service.authenticate("usernamePassword", { + const res = await service.initializeAuthentication("usernamePassword", { body: { email: "test@test.com" }, }) @@ -94,7 +94,7 @@ describe("AuthenticationModuleService - AuthProvider", () => { it("fails when no email is given", async () => { await seedDefaultData(testManager) - const res = await service.authenticate("usernamePassword", { + const res = await service.initializeAuthentication("usernamePassword", { body: { password: "supersecret" }, }) @@ -123,7 +123,7 @@ describe("AuthenticationModuleService - AuthProvider", () => { }, ]) - const res = await service.authenticate("usernamePassword", { + const res = await service.initializeAuthentication("usernamePassword", { body: { email: "test@test.com", password: "password", From 87376898a7aaaaa86e5c753a4dfd78bfe557333d Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Wed, 24 Jan 2024 10:11:00 +0800 Subject: [PATCH 51/53] feedback --- packages/authentication/src/providers/google.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index dc4b1b830fd84..37d6911c901ad 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -82,7 +82,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { return { success: false, error: error.message } } - const code = (req.query && req.query.code) || (req.body && req.body.code) + const code = req.query?.code ?? req.body?.code return await this.validateCallback(code, config) } @@ -155,10 +155,11 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { } private originalURL(req: AuthenticationInput) { - const tls = req.connection.encrypted, - host = req.headers.host, - protocol = tls ? "https" : "http", - path = req.url || "" + const tls = req.connection.encrypted + const host = req.headers.host + const protocol = tls ? "https" : "http" + const path = req.url || "" + return protocol + "://" + host + path } From 03475a4fb165de9b46d950a02ffc4cc25eb00591 Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Thu, 25 Jan 2024 11:03:10 +0800 Subject: [PATCH 52/53] rename split methods --- .../authentication/src/providers/google.ts | 8 +++--- .../src/providers/username-password.ts | 6 ++--- .../src/services/authentication-module.ts | 25 +++---------------- packages/types/src/authentication/service.ts | 6 ++--- .../abstract-authentication-provider.ts | 4 +-- 5 files changed, 15 insertions(+), 34 deletions(-) diff --git a/packages/authentication/src/providers/google.ts b/packages/authentication/src/providers/google.ts index 5586b39931ef3..a1d7cd48bc3d0 100644 --- a/packages/authentication/src/providers/google.ts +++ b/packages/authentication/src/providers/google.ts @@ -43,7 +43,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { this.authProviderService_ = authProviderService } - async initiateAuthentication( + async authenticate( req: AuthenticationInput ): Promise { if (req.query?.error) { @@ -64,7 +64,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { return this.getRedirect(config) } - async authenticateCallback( + async validateCallback( req: AuthenticationInput ): Promise { if (req.query && req.query.error) { @@ -84,7 +84,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { const code = req.query?.code ?? req.body?.code - return await this.validateCallback(code, config) + return await this.validateCallbackToken(code, config) } // abstractable @@ -119,7 +119,7 @@ class GoogleProvider extends AbstractAuthenticationModuleProvider { } // abstractable - private async validateCallback( + private async validateCallbackToken( code: string, { clientID, callbackURL, clientSecret }: ProviderConfig ) { diff --git a/packages/authentication/src/providers/username-password.ts b/packages/authentication/src/providers/username-password.ts index 851704652cc16..905109fb929b6 100644 --- a/packages/authentication/src/providers/username-password.ts +++ b/packages/authentication/src/providers/username-password.ts @@ -1,8 +1,8 @@ -import { AuthenticationResponse } from "@medusajs/types" +import { AbstractAuthenticationModuleProvider, isString } from "@medusajs/utils" import { AuthUserService } from "@services" +import { AuthenticationResponse } from "@medusajs/types" import Scrypt from "scrypt-kdf" -import { AbstractAuthenticationModuleProvider, isString } from "@medusajs/utils" class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { public static PROVIDER = "usernamePassword" @@ -16,7 +16,7 @@ class UsernamePasswordProvider extends AbstractAuthenticationModuleProvider { this.authUserSerivce_ = AuthUserService } - async initiateAuthentication( + async authenticate( userData: Record ): Promise { const { email, password } = userData.body diff --git a/packages/authentication/src/services/authentication-module.ts b/packages/authentication/src/services/authentication-module.ts index 7b086f666c8a8..be6d1216ac470 100644 --- a/packages/authentication/src/services/authentication-module.ts +++ b/packages/authentication/src/services/authentication-module.ts @@ -368,26 +368,7 @@ export default class AuthenticationModuleService< return containerProvider } - @InjectTransactionManager("baseRepository_") async authenticate( - provider: string, - authenticationData: Record, - @MedusaContext() sharedContext: Context = {} - ): Promise { - let registeredProvider - - try { - await this.retrieveAuthProvider(provider, {}) - - registeredProvider = this.getRegisteredAuthenticationProvider(provider) - - return await registeredProvider.authenticate(authenticationData) - } catch (error) { - return { success: false, error: error.message } - } - } - - async authenticateCallback( provider: string, authenticationData: Record ): Promise { @@ -397,13 +378,13 @@ export default class AuthenticationModuleService< const registeredProvider = this.getRegisteredAuthenticationProvider(provider) - return await registeredProvider.authenticateCallback(authenticationData) + return await registeredProvider.authenticate(authenticationData) } catch (error) { return { success: false, error: error.message } } } - async initializeAuthentication( + async validateCallback( provider: string, authenticationData: Record ): Promise { @@ -413,7 +394,7 @@ export default class AuthenticationModuleService< const registeredProvider = this.getRegisteredAuthenticationProvider(provider) - return await registeredProvider.initiateAuthentication(authenticationData) + return await registeredProvider.validateCallback(authenticationData) } catch (error) { return { success: false, error: error.message } } diff --git a/packages/types/src/authentication/service.ts b/packages/types/src/authentication/service.ts index 144f5b929a24c..bd2c8728e790e 100644 --- a/packages/types/src/authentication/service.ts +++ b/packages/types/src/authentication/service.ts @@ -1,7 +1,7 @@ import { - AuthenticationResponse, AuthProviderDTO, AuthUserDTO, + AuthenticationResponse, CreateAuthProviderDTO, CreateAuthUserDTO, FilterableAuthProviderProps, @@ -15,12 +15,12 @@ import { FindConfig } from "../common" import { IModuleService } from "../modules-sdk" export interface IAuthenticationModuleService extends IModuleService { - initializeAuthentication( + authenticate( provider: string, providerData: Record ): Promise - authenticateCallback( + validateCallback( provider: string, providerData: Record ): Promise diff --git a/packages/utils/src/authentication/abstract-authentication-provider.ts b/packages/utils/src/authentication/abstract-authentication-provider.ts index 128b7a6536117..bfa21be4d84b9 100644 --- a/packages/utils/src/authentication/abstract-authentication-provider.ts +++ b/packages/utils/src/authentication/abstract-authentication-provider.ts @@ -13,11 +13,11 @@ export abstract class AbstractAuthenticationModuleProvider { .DISPLAY_NAME } - abstract initiateAuthentication( + abstract authenticate( data: Record ): Promise - public authenticateCallback( + public validateCallback( data: Record ): Promise { throw new Error( From 0c2a711b75c87e4edbd6b3737551badc7e8a89af Mon Sep 17 00:00:00 2001 From: Philip Korsholm Date: Thu, 25 Jan 2024 12:21:14 +0800 Subject: [PATCH 53/53] fix provider integration test --- .../services/providers/username-password.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts b/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts index ab28ef21834c2..3f5ec7da6dc55 100644 --- a/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts +++ b/packages/authentication/integration-tests/__tests__/services/providers/username-password.spec.ts @@ -68,7 +68,7 @@ describe("AuthenticationModuleService - AuthProvider", () => { }, ]) - const res = await service.initializeAuthentication("usernamePassword", { + const res = await service.authenticate("usernamePassword", { body: { email: "test@test.com", password: password, @@ -89,7 +89,7 @@ describe("AuthenticationModuleService - AuthProvider", () => { await seedDefaultData(testManager) - const res = await service.initializeAuthentication("usernamePassword", { + const res = await service.authenticate("usernamePassword", { body: { email: "test@test.com" }, }) @@ -102,7 +102,7 @@ describe("AuthenticationModuleService - AuthProvider", () => { it("fails when no email is given", async () => { await seedDefaultData(testManager) - const res = await service.initializeAuthentication("usernamePassword", { + const res = await service.authenticate("usernamePassword", { body: { password: "supersecret" }, }) @@ -131,7 +131,7 @@ describe("AuthenticationModuleService - AuthProvider", () => { }, ]) - const res = await service.initializeAuthentication("usernamePassword", { + const res = await service.authenticate("usernamePassword", { body: { email: "test@test.com", password: "password",