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:
Stevche Radevski
2024-05-23 20:56:40 +02:00
committed by GitHub
parent 7b0cfe3b77
commit 8a070d5d85
100 changed files with 991 additions and 1005 deletions

View File

@@ -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[]> {

View File

@@ -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]

View File

@@ -0,0 +1 @@
export * from "./default-provider"

View File

@@ -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",
},
])

View File

@@ -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",
})
)
})
}),
})

View File

@@ -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",
})
)
})
})
})
},
})

View File

@@ -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`
)
})
})
})
},
})

View File

@@ -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",
})
})
})
})
},
})