Revamp the authentication setup (#7419)
* feat: Add email pass authentication provider package * feat: Revamp auth module and remove concept of scope * feat: Revamp the auth module to be more standardized in how providers are loaded * feat: Switch from scope to actor type for authentication * feat: Add support for per-actor auth methods * feat: Add emailpass auth provider by default * fix: Add back app_metadata in auth module
This commit is contained in:
@@ -8,18 +8,15 @@ export async function createAuthIdentities(
|
||||
id: "test-id",
|
||||
entity_id: "test-id",
|
||||
provider: "manual",
|
||||
scope: "store",
|
||||
},
|
||||
{
|
||||
id: "test-id-1",
|
||||
entity_id: "test-id-1",
|
||||
provider: "manual",
|
||||
scope: "store",
|
||||
},
|
||||
{
|
||||
entity_id: "test-id-2",
|
||||
provider: "store",
|
||||
scope: "store",
|
||||
},
|
||||
]
|
||||
): Promise<AuthIdentity[]> {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
AuthIdentityDTO,
|
||||
AuthIdentityProviderService,
|
||||
AuthenticationInput,
|
||||
AuthenticationResponse,
|
||||
} from "@medusajs/types"
|
||||
import { AbstractAuthModuleProvider, MedusaError } from "@medusajs/utils"
|
||||
|
||||
export class AuthServiceFixtures extends AbstractAuthModuleProvider {
|
||||
constructor() {
|
||||
super(
|
||||
{},
|
||||
{ provider: "plaintextpass", displayName: "plaintextpass Fixture" }
|
||||
)
|
||||
}
|
||||
|
||||
async authenticate(
|
||||
authenticationData: AuthenticationInput,
|
||||
service: AuthIdentityProviderService
|
||||
): Promise<AuthenticationResponse> {
|
||||
const { email, password } = authenticationData.body ?? {}
|
||||
let authIdentity: AuthIdentityDTO | undefined
|
||||
try {
|
||||
authIdentity = await service.retrieve({
|
||||
entity_id: email,
|
||||
provider: this.provider,
|
||||
})
|
||||
|
||||
if (authIdentity.provider_metadata?.password === password) {
|
||||
return {
|
||||
success: true,
|
||||
authIdentity,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.type === MedusaError.Types.NOT_FOUND) {
|
||||
const createdAuthIdentity = await service.create({
|
||||
entity_id: email,
|
||||
provider: this.provider,
|
||||
provider_metadata: {
|
||||
password,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
authIdentity: createdAuthIdentity,
|
||||
}
|
||||
}
|
||||
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: "Invalid email or password",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const services = [AuthServiceFixtures]
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./default-provider"
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IAuthModuleService } from "@medusajs/types"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { createAuthIdentities } from "../../../__fixtures__/auth-identity"
|
||||
import { createAuthIdentities } from "../../__fixtures__/auth-identity"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
@@ -216,7 +216,6 @@ moduleIntegrationTestRunner({
|
||||
id: "test",
|
||||
provider: "manual",
|
||||
entity_id: "test",
|
||||
scope: "store",
|
||||
},
|
||||
])
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { IAuthModuleService } from "@medusajs/types"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
import { resolve } from "path"
|
||||
|
||||
let moduleOptions = {
|
||||
providers: [
|
||||
{
|
||||
resolve: resolve(
|
||||
process.cwd() +
|
||||
"/integration-tests/__fixtures__/providers/default-provider"
|
||||
),
|
||||
options: {
|
||||
config: {
|
||||
plaintextpass: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.AUTH,
|
||||
moduleOptions,
|
||||
testSuite: ({ service }: SuiteOptions<IAuthModuleService>) =>
|
||||
describe("Auth Module Service", () => {
|
||||
beforeEach(async () => {
|
||||
await service.create({
|
||||
entity_id: "test@admin.com",
|
||||
provider: "plaintextpass",
|
||||
provider_metadata: {
|
||||
password: "plaintext",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("it fails if the provider does not exist", async () => {
|
||||
const err = await service
|
||||
.authenticate("facebook", {
|
||||
body: {
|
||||
email: "test@admin.com",
|
||||
password: "password",
|
||||
},
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err).toEqual({
|
||||
success: false,
|
||||
error: "Could not find a auth provider with id: facebook",
|
||||
})
|
||||
})
|
||||
|
||||
it("successfully calls the provider for authentication if correct password", async () => {
|
||||
const result = await service.authenticate("plaintextpass", {
|
||||
body: {
|
||||
email: "test@admin.com",
|
||||
password: "plaintext",
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
success: true,
|
||||
authIdentity: expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
entity_id: "test@admin.com",
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail if the password is incorrect", async () => {
|
||||
const result = await service
|
||||
.authenticate("plaintextpass", {
|
||||
body: {
|
||||
email: "test@admin.com",
|
||||
password: "incorrect",
|
||||
},
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
error: "Invalid email or password",
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("successfully create a new entity if nonexistent", async () => {
|
||||
const result = await service.authenticate("plaintextpass", {
|
||||
body: {
|
||||
email: "new@admin.com",
|
||||
password: "newpass",
|
||||
},
|
||||
})
|
||||
|
||||
const dbAuthIdentity = await service.retrieve(result.authIdentity.id)
|
||||
|
||||
expect(dbAuthIdentity).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
entity_id: "new@admin.com",
|
||||
})
|
||||
)
|
||||
})
|
||||
}),
|
||||
})
|
||||
@@ -1,231 +0,0 @@
|
||||
import { createAuthIdentities } from "../../../__fixtures__/auth-identity"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { IAuthModuleService } from "@medusajs/types"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.AUTH,
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
service,
|
||||
}: SuiteOptions<IAuthModuleService>) => {
|
||||
describe("AuthIdentity Service", () => {
|
||||
beforeEach(async () => {
|
||||
await createAuthIdentities(MikroOrmWrapper.forkManager())
|
||||
})
|
||||
|
||||
describe("list", () => {
|
||||
it("should list authIdentities", async () => {
|
||||
const authIdentities = await service.list()
|
||||
const serialized = JSON.parse(JSON.stringify(authIdentities))
|
||||
|
||||
expect(serialized).toEqual([
|
||||
expect.objectContaining({
|
||||
provider: "store",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
provider: "manual",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
provider: "manual",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should list authIdentities by id", async () => {
|
||||
const authIdentities = await service.list({
|
||||
id: ["test-id"],
|
||||
})
|
||||
|
||||
expect(authIdentities).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-id",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should list authIdentities by provider_id", async () => {
|
||||
const authIdentities = await service.list({
|
||||
provider: "manual",
|
||||
})
|
||||
|
||||
const serialized = JSON.parse(JSON.stringify(authIdentities))
|
||||
|
||||
expect(serialized).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-id",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-id-1",
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("listAndCount", () => {
|
||||
it("should list authIdentities", async () => {
|
||||
const [authIdentities, count] = await service.listAndCount()
|
||||
const serialized = JSON.parse(JSON.stringify(authIdentities))
|
||||
|
||||
expect(count).toEqual(3)
|
||||
expect(serialized).toEqual([
|
||||
expect.objectContaining({
|
||||
provider: "store",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
provider: "manual",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
provider: "manual",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should listAndCount authIdentities by provider_id", async () => {
|
||||
const [authIdentities, count] = await service.listAndCount({
|
||||
provider: "manual",
|
||||
})
|
||||
|
||||
expect(count).toEqual(2)
|
||||
expect(authIdentities).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-id",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-id-1",
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieve", () => {
|
||||
const id = "test-id"
|
||||
|
||||
it("should return an authIdentity for the given id", async () => {
|
||||
const authIdentity = await service.retrieve(id)
|
||||
|
||||
expect(authIdentity).toEqual(
|
||||
expect.objectContaining({
|
||||
id,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should return authIdentity based on config select param", async () => {
|
||||
const authIdentity = await service.retrieve(id, {
|
||||
select: ["id"],
|
||||
})
|
||||
|
||||
const serialized = JSON.parse(JSON.stringify(authIdentity))
|
||||
|
||||
expect(serialized).toEqual({
|
||||
id,
|
||||
})
|
||||
})
|
||||
|
||||
it("should throw an error when an authIdentity with the given id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"AuthIdentity with id: does-not-exist was not found"
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when a authIdentityId is not provided", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve(undefined as unknown as string)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual("authIdentity - id must be defined")
|
||||
})
|
||||
})
|
||||
|
||||
describe("delete", () => {
|
||||
it("should delete the authIdentities given an id successfully", async () => {
|
||||
const id = "test-id"
|
||||
|
||||
await service.delete([id])
|
||||
|
||||
const authIdentities = await service.list({
|
||||
id: [id],
|
||||
})
|
||||
|
||||
expect(authIdentities).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
it("should throw an error when a id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.update([
|
||||
{
|
||||
id: "does-not-exist",
|
||||
},
|
||||
])
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
'AuthIdentity with id "does-not-exist" not found'
|
||||
)
|
||||
})
|
||||
|
||||
it("should update authIdentity", async () => {
|
||||
const id = "test-id"
|
||||
|
||||
await service.update([
|
||||
{
|
||||
id,
|
||||
provider_metadata: { email: "test@email.com" },
|
||||
},
|
||||
])
|
||||
|
||||
const [authIdentity] = await service.list({ id: [id] })
|
||||
expect(authIdentity).toEqual(
|
||||
expect.objectContaining({
|
||||
provider_metadata: { email: "test@email.com" },
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
it("should create a authIdentity successfully", async () => {
|
||||
await service.create([
|
||||
{
|
||||
id: "test",
|
||||
provider: "manual",
|
||||
entity_id: "test",
|
||||
scope: "store",
|
||||
},
|
||||
])
|
||||
|
||||
const [authIdentity] = await service.list({
|
||||
id: ["test"],
|
||||
})
|
||||
|
||||
expect(authIdentity).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "test",
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
|
||||
import { IAuthModuleService } from "@medusajs/types"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.AUTH,
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
service,
|
||||
}: SuiteOptions<IAuthModuleService>) => {
|
||||
describe("AuthModuleService - AuthProvider", () => {
|
||||
describe("authenticate", () => {
|
||||
it("authenticate validates that a provider is registered in container", async () => {
|
||||
const { success, error } = await service.authenticate(
|
||||
"notRegistered",
|
||||
{} as any
|
||||
)
|
||||
|
||||
expect(success).toBe(false)
|
||||
expect(error).toEqual(
|
||||
"AuthenticationProvider: notRegistered wasn't registered in the module. Have you configured your options correctly?"
|
||||
)
|
||||
})
|
||||
|
||||
it("fails to authenticate using a valid provider with an invalid scope", async () => {
|
||||
const { success, error } = await service.authenticate("emailpass", {
|
||||
authScope: "non-existing",
|
||||
} as any)
|
||||
|
||||
expect(success).toBe(false)
|
||||
expect(error).toEqual(
|
||||
`Scope "non-existing" is not valid for provider emailpass`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -1,133 +0,0 @@
|
||||
import { MedusaModule, Modules } from "@medusajs/modules-sdk"
|
||||
|
||||
import { IAuthModuleService } from "@medusajs/types"
|
||||
import Scrypt from "scrypt-kdf"
|
||||
import { createAuthIdentities } from "../../../__fixtures__/auth-identity"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
const seedDefaultData = async (manager) => {
|
||||
await createAuthIdentities(manager)
|
||||
}
|
||||
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.AUTH,
|
||||
moduleOptions: {
|
||||
providers: [
|
||||
{
|
||||
name: "emailpass",
|
||||
scopes: {
|
||||
admin: {},
|
||||
store: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
service,
|
||||
}: SuiteOptions<IAuthModuleService>) => {
|
||||
describe("AuthModuleService - AuthProvider", () => {
|
||||
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(MikroOrmWrapper.forkManager())
|
||||
await createAuthIdentities(MikroOrmWrapper.forkManager(), [
|
||||
// Add authenticated user
|
||||
{
|
||||
provider: "emailpass",
|
||||
entity_id: email,
|
||||
scope: "store",
|
||||
provider_metadata: {
|
||||
password: passwordHash,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const res = await service.authenticate("emailpass", {
|
||||
body: {
|
||||
email: "test@test.com",
|
||||
password: password,
|
||||
},
|
||||
authScope: "store",
|
||||
} as any)
|
||||
|
||||
expect(res).toEqual({
|
||||
success: true,
|
||||
authIdentity: expect.objectContaining({
|
||||
entity_id: email,
|
||||
provider_metadata: {},
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
it("fails when no password is given", async () => {
|
||||
await seedDefaultData(MikroOrmWrapper.forkManager())
|
||||
|
||||
const res = await service.authenticate("emailpass", {
|
||||
body: { email: "test@test.com" },
|
||||
authScope: "store",
|
||||
} as any)
|
||||
|
||||
expect(res).toEqual({
|
||||
success: false,
|
||||
error: "Password should be a string",
|
||||
})
|
||||
})
|
||||
|
||||
it("fails when no email is given", async () => {
|
||||
await seedDefaultData(MikroOrmWrapper.forkManager())
|
||||
|
||||
const res = await service.authenticate("emailpass", {
|
||||
body: { password: "supersecret" },
|
||||
authScope: "store",
|
||||
} as any)
|
||||
|
||||
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(MikroOrmWrapper.forkManager())
|
||||
await createAuthIdentities(MikroOrmWrapper.forkManager(), [
|
||||
// Add authenticated user
|
||||
{
|
||||
provider: "emailpass",
|
||||
scope: "store",
|
||||
entity_id: email,
|
||||
provider_metadata: {
|
||||
password_hash: passwordHash,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const res = await service.authenticate("emailpass", {
|
||||
body: {
|
||||
email: "test@test.com",
|
||||
password: "password",
|
||||
},
|
||||
authScope: "store",
|
||||
} as any)
|
||||
|
||||
expect(res).toEqual({
|
||||
success: false,
|
||||
error: "Invalid email or password",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user