-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat(authentication): username password provider #6052
Changes from all commits
72f0e14
f8b7089
5adca1c
276f3eb
01578e7
757039d
2adb779
6db3a46
0bf5230
8b2f0f1
eda5647
3e99dff
5bf05cc
f5c4c1f
306c972
9120a7a
1a592d4
3499f4d
228c317
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -229,6 +229,7 @@ describe("AuthUser Service", () => { | |
{ | ||
id: "test", | ||
provider_id: "manual", | ||
entity_id: "test" | ||
}, | ||
]) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
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, | ||
}, | ||
}) | ||
|
||
if(service.__hooks?.onApplicationStart) { | ||
await service.__hooks.onApplicationStart() | ||
} | ||
}) | ||
|
||
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", | ||
entity_id: email, | ||
provider_metadata: { | ||
password: passwordHash, | ||
}, | ||
}, | ||
]) | ||
|
||
const res = await service.authenticate("usernamePassword", { | ||
body: { | ||
email: "test@test.com", | ||
password: password, | ||
}, | ||
}) | ||
|
||
expect(res).toEqual({ | ||
success: true, | ||
authUser: expect.objectContaining({ | ||
entity_id: email, | ||
provider_metadata: { | ||
}, | ||
}), | ||
}) | ||
}) | ||
|
||
it("fails when no password is given", async () => { | ||
const email = "test@test.com" | ||
|
||
await seedDefaultData(testManager) | ||
|
||
const res = await service.authenticate("usernamePassword", { | ||
body: { 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", { | ||
body: { 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", | ||
entity_id: email, | ||
provider_metadata: { | ||
password_hash: passwordHash, | ||
}, | ||
}, | ||
]) | ||
|
||
const res = await service.authenticate("usernamePassword", { | ||
body: { | ||
email: "test@test.com", | ||
password: "password", | ||
}, | ||
}) | ||
|
||
expect(res).toEqual({ | ||
success: false, | ||
error: "Invalid email or password", | ||
}) | ||
}) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,11 @@ | ||
import { LoaderOptions, ModulesSdkTypes } from "@medusajs/types" | ||
import { asClass } from "awilix" | ||
import * as defaultProviders from "@providers" | ||
import { AuthProviderService } from "@services" | ||
import { ServiceTypes } from "@types" | ||
|
||
import { LoaderOptions, ModulesSdkTypes } from "@medusajs/types" | ||
|
||
import { AwilixContainer, ClassOrFunctionReturning, Resolver, asClass, asFunction, asValue } from "awilix" | ||
|
||
export default async ({ | ||
container, | ||
options, | ||
}: LoaderOptions< | ||
| ModulesSdkTypes.ModuleServiceInitializeOptions | ||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions | ||
|
@@ -17,33 +16,22 @@ export default async ({ | |
|
||
const providersToLoad = Object.values(defaultProviders) | ||
|
||
const authProviderService: AuthProviderService = | ||
container.cradle["authProviderService"] | ||
|
||
const providers = await authProviderService.list({ | ||
provider: providersToLoad.map((p) => p.PROVIDER), | ||
}) | ||
|
||
const loadedProviders = new Map(providers.map((p) => [p.provider, p])) | ||
|
||
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)) { | ||
continue | ||
} | ||
container.register({ | ||
[`auth_providers`]: asArray(providersToLoad), | ||
}) | ||
} | ||
|
||
providersToCreate.push({ | ||
provider: provider.PROVIDER, | ||
name: provider.DISPLAY_NAME, | ||
}) | ||
function asArray( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: Seems off to place a generic util like this here |
||
resolvers: (ClassOrFunctionReturning<unknown> | Resolver<unknown>)[] | ||
): { resolve: (container: AwilixContainer) => unknown[] } { | ||
return { | ||
resolve: (container: AwilixContainer) => | ||
resolvers.map((resolver) => container.build(resolver)), | ||
} | ||
|
||
await authProviderService.create(providersToCreate) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { Migration } from '@mikro-orm/migrations'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. comment: You should be able to omit the migration entirely until we have finalized the module. The tests will read the models and do not depend on migration files. |
||
|
||
export class Migration20240115092929 extends Migration { | ||
|
||
async up(): Promise<void> { | ||
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<void> { | ||
this.addSql('alter table "auth_user" drop constraint "IDX_auth_user_provider_entity_id";'); | ||
this.addSql('alter table "auth_user" drop column "entity_id";'); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" }) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: Does this need linting? |
||
export default class AuthUser { | ||
[OptionalProps]: OptionalFields | ||
|
||
@PrimaryKey({ columnType: "text" }) | ||
id!: string | ||
|
||
@Property({ columnType: "text" }) | ||
entity_id: string | ||
|
||
@ManyToOne(() => AuthProvider, { | ||
joinColumn: "provider", | ||
fieldName: "provider_id", | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
todo: I would suggest to start using the medusa app to start modules. you can see how we achieved that in the search pr https://github.com/medusajs/medusa/blob/7de35d84c889070f34dd18255000bb39fba06a72/packages/medusa-test-utils/src/init-modules.ts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @olivermrbl
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not super happy about these service hooks. The consumer of a module service should never have to worry about the providers being created on load - even when using initialize.
However, this concern can be discussed and tackled separately from this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is needed for modules that require something to be executed after all modules are loaded and the application is running.
It was originally created at the index/search module where it reads all the exported joiner configs of loaded modules.
This is not executed by the consumer of the modules. MedusaApp calls it after it has completed loading everything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As we discussed a few times, at some point we need to normalize all entrypoints to be MedusaApp, and discourage the use of
initialize
directly