fix(auth-emailpass): better handle identity with same email error (#13537)

* fix(auth-emailpass): better handle identity with same email error

* add test

* Create blue-laws-argue.md

* check for empty object

* trueeee

* nit

* flip condition
This commit is contained in:
William Bouchard
2025-09-18 13:05:54 -04:00
committed by GitHub
parent a503bbe596
commit 0695c5844f
3 changed files with 126 additions and 27 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/auth-emailpass": patch
---
fix(auth-emailpass): better handle identity with same email error
@@ -1,6 +1,7 @@
import { MedusaError } from "@medusajs/framework/utils" import { MedusaError } from "@medusajs/framework/utils"
import Scrypt from "scrypt-kdf" import Scrypt from "scrypt-kdf"
import { EmailPassAuthService } from "../../src/services/emailpass" import { EmailPassAuthService } from "../../src/services/emailpass"
jest.setTimeout(100000) jest.setTimeout(100000)
describe("Email password auth provider", () => { describe("Email password auth provider", () => {
@@ -152,11 +153,83 @@ describe("Email password auth provider", () => {
) )
}) })
it("throw if auth identity with email already exists", async () => { it("updates identity if it exists but doesnt have app_metadata", async () => {
const authServiceSpies = { const authServiceSpies = {
retrieve: jest.fn().mockImplementation(() => { retrieve: jest.fn().mockImplementation(() => {
return { success: true } return { success: true }
}), }),
update: jest.fn().mockImplementation(() => {
return {
provider_identities: [
{
entity_id: "test@admin.com",
provider: "emailpass",
provider_metadata: {
password: "somehash",
},
},
],
}
}),
}
const resp = await emailpassService.register(
{ body: { email: "test@admin.com", password: "test" } },
authServiceSpies
)
expect(authServiceSpies.retrieve).toHaveBeenCalled()
expect(authServiceSpies.update).toHaveBeenCalled()
expect(resp.authIdentity?.provider_identities?.[0]).toEqual(
expect.objectContaining({
entity_id: "test@admin.com",
provider_metadata: {},
})
)
})
it("updates identity if it exists but app_metadata is empty", async () => {
const authServiceSpies = {
retrieve: jest.fn().mockImplementation(() => {
return { success: true, app_metadata: {} }
}),
update: jest.fn().mockImplementation(() => {
return {
provider_identities: [
{
entity_id: "test@admin.com",
provider: "emailpass",
provider_metadata: {
password: "somehash",
},
},
],
}
}),
}
const resp = await emailpassService.register(
{ body: { email: "test@admin.com", password: "test" } },
authServiceSpies
)
expect(authServiceSpies.retrieve).toHaveBeenCalled()
expect(authServiceSpies.update).toHaveBeenCalled()
expect(resp.authIdentity?.provider_identities?.[0]).toEqual(
expect.objectContaining({
entity_id: "test@admin.com",
provider_metadata: {},
})
)
})
it("throw if auth identity with email already exists and has app_metadata", async () => {
const authServiceSpies = {
retrieve: jest.fn().mockImplementation(() => {
return { success: true, app_metadata: {"user_id": "some-id"} }
}),
create: jest.fn().mockImplementation(() => { create: jest.fn().mockImplementation(() => {
return { return {
provider_identities: [ provider_identities: [
@@ -6,17 +6,20 @@ import {
EmailPassAuthProviderOptions, EmailPassAuthProviderOptions,
Logger, Logger,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import { AbstractAuthModuleProvider, isString, MedusaError, } from "@medusajs/framework/utils"
AbstractAuthModuleProvider,
isString,
MedusaError,
} from "@medusajs/framework/utils"
import Scrypt from "scrypt-kdf" import Scrypt from "scrypt-kdf"
import { isPresent } from "@medusajs/utils"
type InjectedDependencies = { type InjectedDependencies = {
logger: Logger logger: Logger
} }
type AuthIdentityParams = {
email: string;
password: string;
authIdentityService: AuthIdentityProviderService
}
interface LocalServiceConfig extends EmailPassAuthProviderOptions {} interface LocalServiceConfig extends EmailPassAuthProviderOptions {}
export class EmailPassAuthService extends AbstractAuthModuleProvider { export class EmailPassAuthService extends AbstractAuthModuleProvider {
@@ -79,25 +82,6 @@ export class EmailPassAuthService extends AbstractAuthModuleProvider {
} }
} }
protected async createAuthIdentity({ email, password, authIdentityService }) {
const passwordHash = await this.hashPassword(password)
const createdAuthIdentity = await authIdentityService.create({
entity_id: email,
provider_metadata: {
password: passwordHash,
},
})
const copy = JSON.parse(JSON.stringify(createdAuthIdentity))
const providerIdentity = copy.provider_identities?.find(
(pi) => pi.provider === this.provider
)!
delete providerIdentity.provider_metadata?.password
return copy
}
async authenticate( async authenticate(
userData: AuthenticationInput, userData: AuthenticationInput,
authIdentityService: AuthIdentityProviderService authIdentityService: AuthIdentityProviderService
@@ -185,17 +169,31 @@ export class EmailPassAuthService extends AbstractAuthModuleProvider {
} }
try { try {
await authIdentityService.retrieve({ const identity = await authIdentityService.retrieve({
entity_id: email, entity_id: email,
}) })
// If app_metadata is not defined or empty, it means no actor was assigned to the auth_identity yet (still "claimable")
if (!isPresent(identity.app_metadata)) {
const updatedAuthIdentity = await this.upsertAuthIdentity('update', {
email,
password,
authIdentityService,
})
return {
success: true,
authIdentity: updatedAuthIdentity,
}
}
return { return {
success: false, success: false,
error: "Identity with email already exists", error: "Identity with email already exists",
} }
} catch (error) { } catch (error) {
if (error.type === MedusaError.Types.NOT_FOUND) { if (error.type === MedusaError.Types.NOT_FOUND) {
const createdAuthIdentity = await this.createAuthIdentity({ const createdAuthIdentity = await this.upsertAuthIdentity('create', {
email, email,
password, password,
authIdentityService, authIdentityService,
@@ -210,4 +208,27 @@ export class EmailPassAuthService extends AbstractAuthModuleProvider {
return { success: false, error: error.message } return { success: false, error: error.message }
} }
} }
private async upsertAuthIdentity(type: 'update' | 'create', { email, password, authIdentityService }: AuthIdentityParams) {
const passwordHash = await this.hashPassword(password)
const authIdentity = type === 'create' ? await authIdentityService.create({
entity_id: email,
provider_metadata: {
password: passwordHash,
},
}) : await authIdentityService.update(email, {
provider_metadata: {
password: passwordHash,
},
})
const copy = JSON.parse(JSON.stringify(authIdentity))
const providerIdentity = copy.provider_identities?.find(
(pi) => pi.provider === this.provider
)!
delete providerIdentity.provider_metadata?.password
return copy
}
} }