feat: Separate registration from authentication in auth domain (#8683)
* wip * feat: Introduce register * fix: user command * fix: Invite HTTP tests * fix: Auth tests * fix: Invite modules tests
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
import {
|
import {
|
||||||
adminHeaders,
|
adminHeaders,
|
||||||
createAdminUser,
|
createAdminUser,
|
||||||
} from "../../../../helpers/create-admin-user"
|
} from "../../../../helpers/create-admin-user"
|
||||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
@@ -12,27 +12,50 @@ medusaIntegrationTestRunner({
|
|||||||
await createAdminUser(dbConnection, adminHeaders, getContainer())
|
await createAdminUser(dbConnection, adminHeaders, getContainer())
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: This test won't work since we don't allow creating a user through HTTP. We need to have the invite flow plugged in here.
|
it("Invite + registration + authentication flow", async () => {
|
||||||
it.skip("test the entire authentication flow", async () => {
|
// Create invite
|
||||||
// BREAKING: `/admin/auth` changes to `/auth/user/emailpass`
|
const { token: inviteToken } = (
|
||||||
const signup = await api.post("/auth/user/emailpass", {
|
await api.post(
|
||||||
|
"/admin/invites",
|
||||||
|
{ email: "newadmin@medusa.js" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.invite
|
||||||
|
|
||||||
|
// Register identity
|
||||||
|
const signup = await api.post("/auth/user/emailpass/register", {
|
||||||
email: "newadmin@medusa.js",
|
email: "newadmin@medusa.js",
|
||||||
password: "secret_password",
|
password: "secret_password",
|
||||||
})
|
})
|
||||||
|
|
||||||
//BREAKING: In V2, we respond with a JWT token instead of the user object, and a session is not created. you need to call `/auth/session` to create a session
|
|
||||||
expect(signup.status).toEqual(200)
|
expect(signup.status).toEqual(200)
|
||||||
expect(signup.data).toEqual({ token: expect.any(String) })
|
expect(signup.data).toEqual({ token: expect.any(String) })
|
||||||
|
|
||||||
// BREAKING: IN V2 creating a user is separated from creating an auth identity
|
// Accept invite
|
||||||
const createdUser = await api.post(
|
const response = await api.post(
|
||||||
"/admin/users",
|
`/admin/invites/accept?token=${inviteToken}`,
|
||||||
{ email: "newadmin@medusa.js" },
|
{
|
||||||
{ headers: { authorization: `Bearer ${signup.data.token}` } }
|
email: "newadmin@medusa.js",
|
||||||
|
first_name: "John",
|
||||||
|
last_name: "Doe",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${signup.data.token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
expect(createdUser.status).toEqual(200)
|
|
||||||
expect(createdUser.data.user.email).toEqual("newadmin@medusa.js")
|
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data).toEqual({
|
||||||
|
user: expect.objectContaining({
|
||||||
|
email: "newadmin@medusa.js",
|
||||||
|
first_name: "John",
|
||||||
|
last_name: "Doe",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sign in
|
||||||
const login = await api.post("/auth/user/emailpass", {
|
const login = await api.post("/auth/user/emailpass", {
|
||||||
email: "newadmin@medusa.js",
|
email: "newadmin@medusa.js",
|
||||||
password: "secret_password",
|
password: "secret_password",
|
||||||
@@ -40,6 +63,7 @@ medusaIntegrationTestRunner({
|
|||||||
expect(login.status).toEqual(200)
|
expect(login.status).toEqual(200)
|
||||||
expect(login.data).toEqual({ token: expect.any(String) })
|
expect(login.data).toEqual({ token: expect.any(String) })
|
||||||
|
|
||||||
|
// Convert token to session
|
||||||
const createSession = await api.post(
|
const createSession = await api.post(
|
||||||
"/auth/session",
|
"/auth/session",
|
||||||
{},
|
{},
|
||||||
@@ -47,7 +71,7 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
expect(createSession.status).toEqual(200)
|
expect(createSession.status).toEqual(200)
|
||||||
|
|
||||||
// extract cookie
|
// Extract cookie
|
||||||
const [cookie] = createSession.headers["set-cookie"][0].split(";")
|
const [cookie] = createSession.headers["set-cookie"][0].split(";")
|
||||||
expect(cookie).toEqual(expect.stringContaining("connect.sid"))
|
expect(cookie).toEqual(expect.stringContaining("connect.sid"))
|
||||||
|
|
||||||
@@ -55,23 +79,49 @@ medusaIntegrationTestRunner({
|
|||||||
headers: { Cookie: cookie },
|
headers: { Cookie: cookie },
|
||||||
}
|
}
|
||||||
|
|
||||||
// perform cookie authenticated request
|
// Perform cookie authenticated request
|
||||||
const authedRequest = await api.get(
|
const authedRequest = await api.get(
|
||||||
"/admin/products?limit=1",
|
"/admin/products?limit=1",
|
||||||
cookieHeader
|
cookieHeader
|
||||||
)
|
)
|
||||||
expect(authedRequest.status).toEqual(200)
|
expect(authedRequest.status).toEqual(200)
|
||||||
|
|
||||||
// sign out
|
// Sign out
|
||||||
const signOutRequest = await api.delete("/auth/session", cookieHeader)
|
const signOutRequest = await api.delete("/auth/session", cookieHeader)
|
||||||
expect(signOutRequest.status).toEqual(200)
|
expect(signOutRequest.status).toEqual(200)
|
||||||
|
|
||||||
// attempt to perform authenticated request
|
// Attempt to perform authenticated request
|
||||||
const unAuthedRequest = await api
|
const unAuthedRequest = await api
|
||||||
.get("/admin/products?limit=1", cookieHeader)
|
.get("/admin/products?limit=1", cookieHeader)
|
||||||
.catch((e) => e)
|
.catch((e) => e)
|
||||||
|
|
||||||
expect(unAuthedRequest.response.status).toEqual(401)
|
expect(unAuthedRequest.response.status).toEqual(401)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should respond with 401 on register, if email already exists", async () => {
|
||||||
|
const signup = await api
|
||||||
|
.post("/auth/user/emailpass/register", {
|
||||||
|
email: "admin@medusa.js",
|
||||||
|
password: "secret_password",
|
||||||
|
})
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(signup.response.status).toEqual(401)
|
||||||
|
expect(signup.response.data.message).toEqual(
|
||||||
|
"Identity with email already exists"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should respond with 401 on sign in, if email does not exist", async () => {
|
||||||
|
const signup = await api
|
||||||
|
.post("/auth/user/emailpass", {
|
||||||
|
email: "john@doe.com",
|
||||||
|
password: "secret_password",
|
||||||
|
})
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(signup.response.status).toEqual(401)
|
||||||
|
expect(signup.response.data.message).toEqual("Invalid email or password")
|
||||||
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const signup = await api.post("/auth/user/emailpass", {
|
const signup = await api.post("/auth/user/emailpass/register", {
|
||||||
email: "test@medusa-commerce.com",
|
email: "test@medusa-commerce.com",
|
||||||
password: "secret_password",
|
password: "secret_password",
|
||||||
})
|
})
|
||||||
@@ -92,7 +92,7 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
it("should fail to accept an invite given an invalid token", async () => {
|
it("should fail to accept an invite given an invalid token", async () => {
|
||||||
expect.assertions(2)
|
expect.assertions(2)
|
||||||
const signup = await api.post("/auth/user/emailpass", {
|
const signup = await api.post("/auth/user/emailpass/register", {
|
||||||
email: "test@medusa-commerce.com",
|
email: "test@medusa-commerce.com",
|
||||||
password: "secret_password",
|
password: "secret_password",
|
||||||
})
|
})
|
||||||
@@ -119,7 +119,7 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should fail to accept an already accepted invite ", async () => {
|
it("should fail to accept an already accepted invite ", async () => {
|
||||||
const signup = await api.post("/auth/user/emailpass", {
|
const signup = await api.post("/auth/user/emailpass/register", {
|
||||||
email: "test@medusa-commerce.com",
|
email: "test@medusa-commerce.com",
|
||||||
password: "secret_password",
|
password: "secret_password",
|
||||||
})
|
})
|
||||||
@@ -135,7 +135,7 @@ medusaIntegrationTestRunner({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const signupAgain = await api.post("/auth/user/emailpass", {
|
const signupAgain = await api.post("/auth/user/emailpass/register", {
|
||||||
email: "another-test@medusa-commerce.com",
|
email: "another-test@medusa-commerce.com",
|
||||||
password: "secret_password",
|
password: "secret_password",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should fail to accept an invite with an invalid invite token", async () => {
|
it("should fail to accept an invite with an invalid invite token", async () => {
|
||||||
const authResponse = await api.post(`/auth/user/emailpass`, {
|
const authResponse = await api.post(`/auth/user/emailpass/register`, {
|
||||||
email: "potential_member@test.com",
|
email: "potential_member@test.com",
|
||||||
password: "supersecret",
|
password: "supersecret",
|
||||||
})
|
})
|
||||||
@@ -58,7 +58,7 @@ medusaIntegrationTestRunner({
|
|||||||
email: "potential_member@test.com",
|
email: "potential_member@test.com",
|
||||||
})
|
})
|
||||||
|
|
||||||
const authResponse = await api.post(`/auth/user/emailpass`, {
|
const authResponse = await api.post(`/auth/user/emailpass/register`, {
|
||||||
email: "potential_member@test.com",
|
email: "potential_member@test.com",
|
||||||
password: "supersecret",
|
password: "supersecret",
|
||||||
})
|
})
|
||||||
@@ -92,7 +92,7 @@ medusaIntegrationTestRunner({
|
|||||||
email: "potential_member@test.com",
|
email: "potential_member@test.com",
|
||||||
})
|
})
|
||||||
|
|
||||||
const authResponse = await api.post(`/auth/user/emailpass`, {
|
const authResponse = await api.post(`/auth/user/emailpass/register`, {
|
||||||
email: "some-email@test.com",
|
email: "some-email@test.com",
|
||||||
password: "supersecret",
|
password: "supersecret",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { UseMutationOptions, useMutation } from "@tanstack/react-query"
|
|||||||
import { sdk } from "../../lib/client"
|
import { sdk } from "../../lib/client"
|
||||||
import { EmailPassReq } from "../../types/api-payloads"
|
import { EmailPassReq } from "../../types/api-payloads"
|
||||||
|
|
||||||
export const useEmailPassLogin = (
|
export const useSignInWithEmailPassword = (
|
||||||
options?: UseMutationOptions<void, Error, EmailPassReq>
|
options?: UseMutationOptions<void, Error, EmailPassReq>
|
||||||
) => {
|
) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
@@ -15,21 +15,21 @@ export const useEmailPassLogin = (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useSignUpWithEmailPass = (
|
||||||
|
options?: UseMutationOptions<string, Error, EmailPassReq>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) => sdk.auth.register("user", "emailpass", payload),
|
||||||
|
onSuccess: async (data, variables, context) => {
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const useLogout = (options?: UseMutationOptions<void, Error>) => {
|
export const useLogout = (options?: UseMutationOptions<void, Error>) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: () => sdk.auth.logout(),
|
mutationFn: () => sdk.auth.logout(),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCreateAuthUser = (
|
|
||||||
options?: UseMutationOptions<{ token: string }, Error, EmailPassReq>
|
|
||||||
) => {
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: (payload) => sdk.auth.create("user", "emailpass", payload),
|
|
||||||
onSuccess: async (data, variables, context) => {
|
|
||||||
options?.onSuccess?.(data, variables, context)
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Link, useSearchParams } from "react-router-dom"
|
|||||||
import * as z from "zod"
|
import * as z from "zod"
|
||||||
import { Form } from "../../components/common/form"
|
import { Form } from "../../components/common/form"
|
||||||
import { LogoBox } from "../../components/common/logo-box"
|
import { LogoBox } from "../../components/common/logo-box"
|
||||||
import { useCreateAuthUser } from "../../hooks/api/auth"
|
import { useSignUpWithEmailPass } from "../../hooks/api/auth"
|
||||||
import { useAcceptInvite } from "../../hooks/api/invites"
|
import { useAcceptInvite } from "../../hooks/api/invites"
|
||||||
import { isFetchError } from "../../lib/is-fetch-error"
|
import { isFetchError } from "../../lib/is-fetch-error"
|
||||||
|
|
||||||
@@ -204,15 +204,15 @@ const CreateView = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync: createAuthUser, isPending: isCreatingAuthUser } =
|
const { mutateAsync: signUpEmailPass, isPending: isCreatingAuthUser } =
|
||||||
useCreateAuthUser()
|
useSignUpWithEmailPass()
|
||||||
|
|
||||||
const { mutateAsync: acceptInvite, isPending: isAcceptingInvite } =
|
const { mutateAsync: acceptInvite, isPending: isAcceptingInvite } =
|
||||||
useAcceptInvite(token)
|
useAcceptInvite(token)
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
try {
|
try {
|
||||||
const { token: authToken } = await createAuthUser({
|
const authToken = await signUpEmailPass({
|
||||||
email: data.email,
|
email: data.email,
|
||||||
password: data.password,
|
password: data.password,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import * as z from "zod"
|
|||||||
import { Divider } from "../../components/common/divider"
|
import { Divider } from "../../components/common/divider"
|
||||||
import { Form } from "../../components/common/form"
|
import { Form } from "../../components/common/form"
|
||||||
import { LogoBox } from "../../components/common/logo-box"
|
import { LogoBox } from "../../components/common/logo-box"
|
||||||
import { useEmailPassLogin } from "../../hooks/api/auth"
|
import { useSignInWithEmailPassword } from "../../hooks/api/auth"
|
||||||
import { isAxiosError } from "../../lib/is-axios-error"
|
import { isAxiosError } from "../../lib/is-axios-error"
|
||||||
|
|
||||||
import after from "virtual:medusa/widgets/login/after"
|
import after from "virtual:medusa/widgets/login/after"
|
||||||
@@ -34,8 +34,7 @@ export const Login = () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Update when more than emailpass is supported
|
const { mutateAsync, isPending } = useSignInWithEmailPassword()
|
||||||
const { mutateAsync, isPending } = useEmailPassLogin()
|
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async ({ email, password }) => {
|
const handleSubmit = form.handleSubmit(async ({ email, password }) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -10,6 +10,24 @@ export class Auth {
|
|||||||
this.config = config
|
this.config = config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
register = async (
|
||||||
|
actor: "customer" | "user",
|
||||||
|
method: "emailpass",
|
||||||
|
payload: { email: string; password: string }
|
||||||
|
) => {
|
||||||
|
const { token } = await this.client.fetch<{ token: string }>(
|
||||||
|
`/auth/${actor}/${method}/register`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: payload,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.client.setToken(token)
|
||||||
|
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
login = async (
|
login = async (
|
||||||
actor: "customer" | "user",
|
actor: "customer" | "user",
|
||||||
method: "emailpass",
|
method: "emailpass",
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ export interface IAuthProvider {
|
|||||||
data: AuthenticationInput,
|
data: AuthenticationInput,
|
||||||
authIdentityProviderService: AuthIdentityProviderService
|
authIdentityProviderService: AuthIdentityProviderService
|
||||||
): Promise<AuthenticationResponse>
|
): Promise<AuthenticationResponse>
|
||||||
|
register(
|
||||||
|
data: AuthenticationInput,
|
||||||
|
authIdentityProviderService: AuthIdentityProviderService
|
||||||
|
): Promise<AuthenticationResponse>
|
||||||
validateCallback(
|
validateCallback(
|
||||||
data: AuthenticationInput,
|
data: AuthenticationInput,
|
||||||
authIdentityProviderService: AuthIdentityProviderService
|
authIdentityProviderService: AuthIdentityProviderService
|
||||||
|
|||||||
@@ -46,7 +46,12 @@ export interface IAuthModuleService extends IModuleService {
|
|||||||
*/
|
*/
|
||||||
authenticate(
|
authenticate(
|
||||||
provider: string,
|
provider: string,
|
||||||
providerData: AuthenticationInput
|
providerData: AuthenticationInput,
|
||||||
|
): Promise<AuthenticationResponse>
|
||||||
|
|
||||||
|
register(
|
||||||
|
provider: string,
|
||||||
|
providerData: AuthenticationInput,
|
||||||
): Promise<AuthenticationResponse>
|
): Promise<AuthenticationResponse>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,39 +7,39 @@ import {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* ### constructor
|
* ### constructor
|
||||||
*
|
*
|
||||||
* The constructor allows you to access resources from the module's container using the first parameter,
|
* The constructor allows you to access resources from the module's container using the first parameter,
|
||||||
* and the module's options using the second parameter.
|
* and the module's options using the second parameter.
|
||||||
*
|
*
|
||||||
* If you're creating a client or establishing a connection with a third-party service, do it in the constructor.
|
* If you're creating a client or establishing a connection with a third-party service, do it in the constructor.
|
||||||
*
|
*
|
||||||
* In the constructor, you must pass to the parent constructor two parameters:
|
* In the constructor, you must pass to the parent constructor two parameters:
|
||||||
*
|
*
|
||||||
* 1. The first one is an empty object.
|
* 1. The first one is an empty object.
|
||||||
* 2. The second is an object having two properties:
|
* 2. The second is an object having two properties:
|
||||||
* - `provider`: The ID of the provider. For example, `emailpass`.
|
* - `provider`: The ID of the provider. For example, `emailpass`.
|
||||||
* - `displayName`: The label or displayable name of the provider. For example, `Email and Password Authentication`.
|
* - `displayName`: The label or displayable name of the provider. For example, `Email and Password Authentication`.
|
||||||
*
|
*
|
||||||
* #### Example
|
* #### Example
|
||||||
*
|
*
|
||||||
* ```ts
|
* ```ts
|
||||||
* import { AbstractAuthModuleProvider } from "@medusajs/utils"
|
* import { AbstractAuthModuleProvider } from "@medusajs/utils"
|
||||||
* import { Logger } from "@medusajs/types"
|
* import { Logger } from "@medusajs/types"
|
||||||
*
|
*
|
||||||
* type InjectedDependencies = {
|
* type InjectedDependencies = {
|
||||||
* logger: Logger
|
* logger: Logger
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* type Options = {
|
* type Options = {
|
||||||
* apiKey: string
|
* apiKey: string
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* class MyAuthProviderService extends AbstractAuthModuleProvider {
|
* class MyAuthProviderService extends AbstractAuthModuleProvider {
|
||||||
* protected logger_: Logger
|
* protected logger_: Logger
|
||||||
* protected options_: Options
|
* protected options_: Options
|
||||||
* // assuming you're initializing a client
|
* // assuming you're initializing a client
|
||||||
* protected client
|
* protected client
|
||||||
*
|
*
|
||||||
* constructor (
|
* constructor (
|
||||||
* { logger }: InjectedDependencies,
|
* { logger }: InjectedDependencies,
|
||||||
* options: Options
|
* options: Options
|
||||||
@@ -51,17 +51,17 @@ import {
|
|||||||
* displayName: "My Custom Authentication"
|
* displayName: "My Custom Authentication"
|
||||||
* }
|
* }
|
||||||
* )
|
* )
|
||||||
*
|
*
|
||||||
* this.logger_ = logger
|
* this.logger_ = logger
|
||||||
* this.options_ = options
|
* this.options_ = options
|
||||||
*
|
*
|
||||||
* // assuming you're initializing a client
|
* // assuming you're initializing a client
|
||||||
* this.client = new Client(options)
|
* this.client = new Client(options)
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* // ...
|
* // ...
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* export default MyAuthProviderService
|
* export default MyAuthProviderService
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@@ -93,7 +93,7 @@ export abstract class AbstractAuthModuleProvider implements IAuthProvider {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @ignore
|
* @ignore
|
||||||
*
|
*
|
||||||
* @privateRemarks
|
* @privateRemarks
|
||||||
* Documenting the constructor in the class's TSDocs as it's difficult to relay
|
* Documenting the constructor in the class's TSDocs as it's difficult to relay
|
||||||
* the necessary information with this constructor's signature.
|
* the necessary information with this constructor's signature.
|
||||||
@@ -108,41 +108,41 @@ export abstract class AbstractAuthModuleProvider implements IAuthProvider {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This method authenticates the user.
|
* This method authenticates the user.
|
||||||
*
|
*
|
||||||
* The authentication happens either by directly authenticating or returning a redirect URL to continue
|
* The authentication happens either by directly authenticating or returning a redirect URL to continue
|
||||||
* the authentication with a third party provider.
|
* the authentication with a third party provider.
|
||||||
*
|
*
|
||||||
* @param {AuthenticationInput} data - The details of the authentication request.
|
* @param {AuthenticationInput} data - The details of the authentication request.
|
||||||
* @param {AuthIdentityProviderService} authIdentityProviderService - The service used to retrieve or
|
* @param {AuthIdentityProviderService} authIdentityProviderService - The service used to retrieve or
|
||||||
* create an auth identity. It has two methods: `create` to create an auth identity,
|
* create an auth identity. It has two methods: `create` to create an auth identity,
|
||||||
* and `retrieve` to retrieve an auth identity. When you authenticate the user, you can create an auth identity
|
* and `retrieve` to retrieve an auth identity. When you authenticate the user, you can create an auth identity
|
||||||
* using this service.
|
* using this service.
|
||||||
* @returns {Promise<AuthenticationResponse>} The authentication response.
|
* @returns {Promise<AuthenticationResponse>} The authentication response.
|
||||||
*
|
*
|
||||||
* @privateRemarks
|
* @privateRemarks
|
||||||
* TODO add a link to the authentication flow document once it's public.
|
* TODO add a link to the authentication flow document once it's public.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* For example, if your authentication provider doesn't require validating a callback:
|
* For example, if your authentication provider doesn't require validating a callback:
|
||||||
*
|
*
|
||||||
* ```ts
|
* ```ts
|
||||||
* import {
|
* import {
|
||||||
* AuthIdentityProviderService,
|
* AuthIdentityProviderService,
|
||||||
* AuthenticationInput,
|
* AuthenticationInput,
|
||||||
* AuthenticationResponse
|
* AuthenticationResponse
|
||||||
* } from "@medusajs/types"
|
* } from "@medusajs/types"
|
||||||
* // ...
|
* // ...
|
||||||
*
|
*
|
||||||
* class MyAuthProviderService extends AbstractAuthModuleProvider {
|
* class MyAuthProviderService extends AbstractAuthModuleProvider {
|
||||||
* // ...
|
* // ...
|
||||||
* async authenticate(
|
* async authenticate(
|
||||||
* data: AuthenticationInput,
|
* data: AuthenticationInput,
|
||||||
* authIdentityProviderService: AuthIdentityProviderService
|
* authIdentityProviderService: AuthIdentityProviderService
|
||||||
* ): Promise<AuthenticationResponse> {
|
* ): Promise<AuthenticationResponse> {
|
||||||
* const isAuthenticated = false
|
* const isAuthenticated = false
|
||||||
* // TODO perform custom logic to authenticate the user
|
* // TODO perform custom logic to authenticate the user
|
||||||
* // ...
|
* // ...
|
||||||
*
|
*
|
||||||
* if (!isAuthenticated) {
|
* if (!isAuthenticated) {
|
||||||
* // if the authentication didn't succeed, return
|
* // if the authentication didn't succeed, return
|
||||||
* // an object of the following format
|
* // an object of the following format
|
||||||
@@ -151,11 +151,11 @@ export abstract class AbstractAuthModuleProvider implements IAuthProvider {
|
|||||||
* error: "Incorrect credentials"
|
* error: "Incorrect credentials"
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* // authentication is successful, create an auth identity
|
* // authentication is successful, create an auth identity
|
||||||
* // if doesn't exist
|
* // if doesn't exist
|
||||||
* let authIdentity
|
* let authIdentity
|
||||||
*
|
*
|
||||||
* try {
|
* try {
|
||||||
* authIdentity = await authIdentityProviderService.retrieve({
|
* authIdentity = await authIdentityProviderService.retrieve({
|
||||||
* entity_id: data.body.email, // email or some ID
|
* entity_id: data.body.email, // email or some ID
|
||||||
@@ -171,7 +171,7 @@ export abstract class AbstractAuthModuleProvider implements IAuthProvider {
|
|||||||
* }
|
* }
|
||||||
* })
|
* })
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* return {
|
* return {
|
||||||
* success: true,
|
* success: true,
|
||||||
* authIdentity
|
* authIdentity
|
||||||
@@ -179,27 +179,27 @@ export abstract class AbstractAuthModuleProvider implements IAuthProvider {
|
|||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* If your authentication provider requires validating callback:
|
* If your authentication provider requires validating callback:
|
||||||
*
|
*
|
||||||
* ```ts
|
* ```ts
|
||||||
* import {
|
* import {
|
||||||
* AuthIdentityProviderService,
|
* AuthIdentityProviderService,
|
||||||
* AuthenticationInput,
|
* AuthenticationInput,
|
||||||
* AuthenticationResponse
|
* AuthenticationResponse
|
||||||
* } from "@medusajs/types"
|
* } from "@medusajs/types"
|
||||||
* // ...
|
* // ...
|
||||||
*
|
*
|
||||||
* class MyAuthProviderService extends AbstractAuthModuleProvider {
|
* class MyAuthProviderService extends AbstractAuthModuleProvider {
|
||||||
* // ...
|
* // ...
|
||||||
* async authenticate(
|
* async authenticate(
|
||||||
* data: AuthenticationInput,
|
* data: AuthenticationInput,
|
||||||
* authIdentityProviderService: AuthIdentityProviderService
|
* authIdentityProviderService: AuthIdentityProviderService
|
||||||
* ): Promise<AuthenticationResponse> {
|
* ): Promise<AuthenticationResponse> {
|
||||||
* const isAuthenticated = false
|
* const isAuthenticated = false
|
||||||
* // TODO perform custom logic to authenticate the user
|
* // TODO perform custom logic to authenticate the user
|
||||||
* // ...
|
* // ...
|
||||||
*
|
*
|
||||||
* if (!isAuthenticated) {
|
* if (!isAuthenticated) {
|
||||||
* // if the authentication didn't succeed, return
|
* // if the authentication didn't succeed, return
|
||||||
* // an object of the following format
|
* // an object of the following format
|
||||||
@@ -208,7 +208,7 @@ export abstract class AbstractAuthModuleProvider implements IAuthProvider {
|
|||||||
* error: "Incorrect credentials"
|
* error: "Incorrect credentials"
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* return {
|
* return {
|
||||||
* success: true,
|
* success: true,
|
||||||
* location: "some-url.com"
|
* location: "some-url.com"
|
||||||
@@ -222,43 +222,52 @@ export abstract class AbstractAuthModuleProvider implements IAuthProvider {
|
|||||||
authIdentityProviderService: AuthIdentityProviderService
|
authIdentityProviderService: AuthIdentityProviderService
|
||||||
): Promise<AuthenticationResponse>
|
): Promise<AuthenticationResponse>
|
||||||
|
|
||||||
|
register(
|
||||||
|
data: AuthenticationInput,
|
||||||
|
authIdentityProviderService: AuthIdentityProviderService
|
||||||
|
): Promise<AuthenticationResponse> {
|
||||||
|
throw new Error(
|
||||||
|
`Method 'register' not implemented for provider ${this.provider}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method validates the callback of an authentication request.
|
* This method validates the callback of an authentication request.
|
||||||
*
|
*
|
||||||
* In an authentication flow that requires performing an action with a third-party service, such as login
|
* In an authentication flow that requires performing an action with a third-party service, such as login
|
||||||
* with a social account, the {@link authenticate} method is called first.
|
* with a social account, the {@link authenticate} method is called first.
|
||||||
*
|
*
|
||||||
* Then, the third-party service redirects to the Medusa application's validate callback API route.
|
* Then, the third-party service redirects to the Medusa application's validate callback API route.
|
||||||
* That route uses this method to authenticate the user.
|
* That route uses this method to authenticate the user.
|
||||||
*
|
*
|
||||||
* @param {AuthenticationInput} data - The details of the authentication request.
|
* @param {AuthenticationInput} data - The details of the authentication request.
|
||||||
* @param {AuthIdentityProviderService} authIdentityProviderService - The service used to retrieve or
|
* @param {AuthIdentityProviderService} authIdentityProviderService - The service used to retrieve or
|
||||||
* create an auth identity. It has two methods: `create` to create an auth identity,
|
* create an auth identity. It has two methods: `create` to create an auth identity,
|
||||||
* and `retrieve` to retrieve an auth identity. When you authenticate the user, you can create an auth identity
|
* and `retrieve` to retrieve an auth identity. When you authenticate the user, you can create an auth identity
|
||||||
* using this service.
|
* using this service.
|
||||||
* @returns {Promise<AuthenticationResponse>} The authentication response.
|
* @returns {Promise<AuthenticationResponse>} The authentication response.
|
||||||
*
|
*
|
||||||
* @privateRemarks
|
* @privateRemarks
|
||||||
* TODO add a link to the authentication flow document once it's public.
|
* TODO add a link to the authentication flow document once it's public.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* import {
|
* import {
|
||||||
* AuthIdentityProviderService,
|
* AuthIdentityProviderService,
|
||||||
* AuthenticationInput,
|
* AuthenticationInput,
|
||||||
* AuthenticationResponse
|
* AuthenticationResponse
|
||||||
* } from "@medusajs/types"
|
* } from "@medusajs/types"
|
||||||
* // ...
|
* // ...
|
||||||
*
|
*
|
||||||
* class MyAuthProviderService extends AbstractAuthModuleProvider {
|
* class MyAuthProviderService extends AbstractAuthModuleProvider {
|
||||||
* // ...
|
* // ...
|
||||||
* async validateCallback(
|
* async validateCallback(
|
||||||
* data: AuthenticationInput,
|
* data: AuthenticationInput,
|
||||||
* authIdentityProviderService: AuthIdentityProviderService
|
* authIdentityProviderService: AuthIdentityProviderService
|
||||||
* ): Promise<AuthenticationResponse> {
|
* ): Promise<AuthenticationResponse> {
|
||||||
* const isAuthenticated = false
|
* const isAuthenticated = false
|
||||||
* // TODO perform custom logic to authenticate the user
|
* // TODO perform custom logic to authenticate the user
|
||||||
* // ...
|
* // ...
|
||||||
*
|
*
|
||||||
* if (!isAuthenticated) {
|
* if (!isAuthenticated) {
|
||||||
* // if the authentication didn't succeed, return
|
* // if the authentication didn't succeed, return
|
||||||
* // an object of the following format
|
* // an object of the following format
|
||||||
@@ -267,11 +276,11 @@ export abstract class AbstractAuthModuleProvider implements IAuthProvider {
|
|||||||
* error: "Something went wrong"
|
* error: "Something went wrong"
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* // authentication is successful, create an auth identity
|
* // authentication is successful, create an auth identity
|
||||||
* // if doesn't exist
|
* // if doesn't exist
|
||||||
* let authIdentity
|
* let authIdentity
|
||||||
*
|
*
|
||||||
* try {
|
* try {
|
||||||
* authIdentity = await authIdentityProviderService.retrieve({
|
* authIdentity = await authIdentityProviderService.retrieve({
|
||||||
* entity_id: data.body.email, // email or some ID
|
* entity_id: data.body.email, // email or some ID
|
||||||
@@ -287,7 +296,7 @@ export abstract class AbstractAuthModuleProvider implements IAuthProvider {
|
|||||||
* }
|
* }
|
||||||
* })
|
* })
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* return {
|
* return {
|
||||||
* success: true,
|
* success: true,
|
||||||
* authIdentity
|
* authIdentity
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import {
|
|||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { NextFunction, RequestHandler } from "express"
|
import { NextFunction, RequestHandler } from "express"
|
||||||
import { JwtPayload, verify } from "jsonwebtoken"
|
import { JwtPayload, verify } from "jsonwebtoken"
|
||||||
|
import { ConfigModule } from "../../config"
|
||||||
import {
|
import {
|
||||||
AuthContext,
|
AuthContext,
|
||||||
AuthenticatedMedusaRequest,
|
AuthenticatedMedusaRequest,
|
||||||
MedusaRequest,
|
MedusaRequest,
|
||||||
MedusaResponse,
|
MedusaResponse,
|
||||||
} from "../types"
|
} from "../types"
|
||||||
import { ConfigModule } from "../../config"
|
|
||||||
|
|
||||||
const SESSION_AUTH = "session"
|
const SESSION_AUTH = "session"
|
||||||
const BEARER_AUTH = "bearer"
|
const BEARER_AUTH = "bearer"
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
AuthenticationInput,
|
||||||
|
ConfigModule,
|
||||||
|
IAuthModuleService,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
MedusaError,
|
||||||
|
ModuleRegistrationName,
|
||||||
|
} from "@medusajs/utils"
|
||||||
|
import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
|
||||||
|
import { generateJwtTokenForAuthIdentity } from "../../../utils/generate-jwt-token"
|
||||||
|
|
||||||
|
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||||
|
const { actor_type, auth_provider } = req.params
|
||||||
|
const config: ConfigModule = req.scope.resolve(
|
||||||
|
ContainerRegistrationKeys.CONFIG_MODULE
|
||||||
|
)
|
||||||
|
|
||||||
|
const authMethodsPerActor =
|
||||||
|
config.projectConfig?.http?.authMethodsPerActor ?? {}
|
||||||
|
// Not having the config defined would allow for all auth providers for the particular actor.
|
||||||
|
if (authMethodsPerActor[actor_type]) {
|
||||||
|
if (!authMethodsPerActor[actor_type].includes(auth_provider)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
|
`The actor type ${actor_type} is not allowed to use the auth provider ${auth_provider}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const service: IAuthModuleService = req.scope.resolve(
|
||||||
|
ModuleRegistrationName.AUTH
|
||||||
|
)
|
||||||
|
|
||||||
|
const authData = {
|
||||||
|
url: req.url,
|
||||||
|
headers: req.headers,
|
||||||
|
query: req.query,
|
||||||
|
body: req.body,
|
||||||
|
protocol: req.protocol,
|
||||||
|
} as AuthenticationInput
|
||||||
|
|
||||||
|
const { success, error, authIdentity } = await service.register(
|
||||||
|
auth_provider,
|
||||||
|
authData
|
||||||
|
)
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
const { http } = config.projectConfig
|
||||||
|
|
||||||
|
const token = generateJwtTokenForAuthIdentity(
|
||||||
|
{
|
||||||
|
authIdentity,
|
||||||
|
actorType: actor_type,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secret: http.jwtSecret,
|
||||||
|
expiresIn: http.jwtExpiresIn,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return res.status(200).json({ token })
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.UNAUTHORIZED,
|
||||||
|
error || "Authentication failed"
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,9 +7,9 @@ import {
|
|||||||
ContainerRegistrationKeys,
|
ContainerRegistrationKeys,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
ModuleRegistrationName,
|
ModuleRegistrationName,
|
||||||
generateJwtToken,
|
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
|
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
|
||||||
|
import { generateJwtTokenForAuthIdentity } from "../../utils/generate-jwt-token"
|
||||||
|
|
||||||
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||||
const { actor_type, auth_provider } = req.params
|
const { actor_type, auth_provider } = req.params
|
||||||
@@ -52,30 +52,16 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
const { http } = req.scope.resolve(
|
const { http } = config.projectConfig
|
||||||
ContainerRegistrationKeys.CONFIG_MODULE
|
|
||||||
).projectConfig
|
|
||||||
|
|
||||||
const entityIdKey = `${actor_type}_id`
|
const token = generateJwtTokenForAuthIdentity(
|
||||||
const entityId = authIdentity?.app_metadata?.[entityIdKey] as
|
|
||||||
| string
|
|
||||||
| undefined
|
|
||||||
const { jwtSecret, jwtExpiresIn } = http
|
|
||||||
|
|
||||||
const token = generateJwtToken(
|
|
||||||
{
|
{
|
||||||
actor_id: entityId ?? "",
|
authIdentity,
|
||||||
actor_type,
|
actorType: actor_type,
|
||||||
auth_identity_id: authIdentity?.id ?? "",
|
|
||||||
app_metadata: {
|
|
||||||
[entityIdKey]: entityId,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// @ts-expect-error
|
secret: http.jwtSecret,
|
||||||
secret: jwtSecret,
|
expiresIn: http.jwtExpiresIn,
|
||||||
// @ts-expect-error
|
|
||||||
expiresIn: jwtExpiresIn,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ export const authRoutesMiddlewares: MiddlewareRoute[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: ["POST"],
|
method: ["POST"],
|
||||||
|
matcher: "/auth/:actor_type/:auth_provider/register",
|
||||||
|
middlewares: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: ["POST"],
|
||||||
|
matcher: "/auth/:actor_type/:auth_provider",
|
||||||
|
middlewares: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: ["GET"],
|
||||||
matcher: "/auth/:actor_type/:auth_provider",
|
matcher: "/auth/:actor_type/:auth_provider",
|
||||||
middlewares: [],
|
middlewares: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { generateJwtToken } from "@medusajs/utils"
|
||||||
|
|
||||||
|
export function generateJwtTokenForAuthIdentity(
|
||||||
|
{ authIdentity, actorType },
|
||||||
|
{ secret, expiresIn }
|
||||||
|
) {
|
||||||
|
const entityIdKey = `${actorType}_id`
|
||||||
|
const entityId = authIdentity?.app_metadata?.[entityIdKey] as
|
||||||
|
| string
|
||||||
|
| undefined
|
||||||
|
|
||||||
|
return generateJwtToken(
|
||||||
|
{
|
||||||
|
actor_id: entityId ?? "",
|
||||||
|
actor_type: actorType,
|
||||||
|
auth_identity_id: authIdentity?.id ?? "",
|
||||||
|
app_metadata: {
|
||||||
|
[entityIdKey]: entityId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secret,
|
||||||
|
expiresIn,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ export default async function ({
|
|||||||
} else {
|
} else {
|
||||||
const user = await userService.createUsers({ email })
|
const user = await userService.createUsers({ email })
|
||||||
|
|
||||||
const { authIdentity, error } = await authService.authenticate(provider, {
|
const { authIdentity, error } = await authService.register(provider, {
|
||||||
body: {
|
body: {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
|
|||||||
@@ -121,6 +121,20 @@ export default class AuthModuleService
|
|||||||
return Array.isArray(data) ? serializedUsers : serializedUsers[0]
|
return Array.isArray(data) ? serializedUsers : serializedUsers[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async register(
|
||||||
|
provider: string,
|
||||||
|
authenticationData: AuthenticationInput
|
||||||
|
): Promise<AuthenticationResponse> {
|
||||||
|
try {
|
||||||
|
return await this.authProviderService_.register(
|
||||||
|
provider,
|
||||||
|
authenticationData,
|
||||||
|
this.getAuthIdentityProviderService(provider)
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
createProviderIdentities(
|
createProviderIdentities(
|
||||||
data: AuthTypes.CreateProviderIdentityDTO[],
|
data: AuthTypes.CreateProviderIdentityDTO[],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
|
AuthIdentityProviderService,
|
||||||
AuthTypes,
|
AuthTypes,
|
||||||
AuthenticationInput,
|
AuthenticationInput,
|
||||||
AuthIdentityProviderService,
|
|
||||||
AuthenticationResponse,
|
AuthenticationResponse,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import { MedusaError } from "@medusajs/utils"
|
import { MedusaError } from "@medusajs/utils"
|
||||||
@@ -42,6 +42,15 @@ export default class AuthProviderService {
|
|||||||
return await providerHandler.authenticate(auth, authIdentityProviderService)
|
return await providerHandler.authenticate(auth, authIdentityProviderService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async register(
|
||||||
|
provider: string,
|
||||||
|
auth: AuthenticationInput,
|
||||||
|
authIdentityProviderService: AuthIdentityProviderService
|
||||||
|
): Promise<AuthenticationResponse> {
|
||||||
|
const providerHandler = this.retrieveProviderRegistration(provider)
|
||||||
|
return await providerHandler.register(auth, authIdentityProviderService)
|
||||||
|
}
|
||||||
|
|
||||||
async validateCallback(
|
async validateCallback(
|
||||||
provider: string,
|
provider: string,
|
||||||
auth: AuthenticationInput,
|
auth: AuthenticationInput,
|
||||||
|
|||||||
+49
-1
@@ -136,7 +136,7 @@ describe("Email password auth provider", () => {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = await emailpassService.authenticate(
|
const resp = await emailpassService.register(
|
||||||
{ body: { email: "test@admin.com", password: "test" } },
|
{ body: { email: "test@admin.com", password: "test" } },
|
||||||
authServiceSpies
|
authServiceSpies
|
||||||
)
|
)
|
||||||
@@ -151,4 +151,52 @@ describe("Email password auth provider", () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("throw if auth identity with email already exists", async () => {
|
||||||
|
const authServiceSpies = {
|
||||||
|
retrieve: jest.fn().mockImplementation(() => {
|
||||||
|
return { success: true }
|
||||||
|
}),
|
||||||
|
create: 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(resp.error).toEqual("Identity with email already exists")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws if auth identity with email doesn't exist", async () => {
|
||||||
|
const authServiceSpies = {
|
||||||
|
retrieve: jest.fn().mockImplementation(() => {
|
||||||
|
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Not found")
|
||||||
|
}),
|
||||||
|
create: jest.fn().mockImplementation(() => {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await emailpassService.authenticate(
|
||||||
|
{ body: { email: "test@admin.com", password: "test" } },
|
||||||
|
authServiceSpies
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(authServiceSpies.retrieve).toHaveBeenCalled()
|
||||||
|
|
||||||
|
expect(resp.error).toEqual("Invalid email or password")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
Logger,
|
|
||||||
EmailPassAuthProviderOptions,
|
|
||||||
AuthenticationResponse,
|
|
||||||
AuthenticationInput,
|
AuthenticationInput,
|
||||||
AuthIdentityProviderService,
|
AuthenticationResponse,
|
||||||
AuthIdentityDTO,
|
AuthIdentityDTO,
|
||||||
|
AuthIdentityProviderService,
|
||||||
|
EmailPassAuthProviderOptions,
|
||||||
|
Logger,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
AbstractAuthModuleProvider,
|
AbstractAuthModuleProvider,
|
||||||
MedusaError,
|
|
||||||
isString,
|
isString,
|
||||||
|
MedusaError,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import Scrypt from "scrypt-kdf"
|
import Scrypt from "scrypt-kdf"
|
||||||
|
|
||||||
@@ -35,6 +35,26 @@ export class EmailPassAuthService extends AbstractAuthModuleProvider {
|
|||||||
this.logger_ = logger
|
this.logger_ = logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async createAuthIdentity({ email, password, authIdentityService }) {
|
||||||
|
const hashConfig = this.config_.hashConfig ?? { logN: 15, r: 8, p: 1 }
|
||||||
|
const passwordHash = await Scrypt.kdf(password, hashConfig)
|
||||||
|
|
||||||
|
const createdAuthIdentity = await authIdentityService.create({
|
||||||
|
entity_id: email,
|
||||||
|
provider_metadata: {
|
||||||
|
password: passwordHash.toString("base64"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
@@ -54,6 +74,7 @@ export class EmailPassAuthService extends AbstractAuthModuleProvider {
|
|||||||
error: "Email should be a string",
|
error: "Email should be a string",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let authIdentity: AuthIdentityDTO | undefined
|
let authIdentity: AuthIdentityDTO | undefined
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -62,25 +83,9 @@ export class EmailPassAuthService extends AbstractAuthModuleProvider {
|
|||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.type === MedusaError.Types.NOT_FOUND) {
|
if (error.type === MedusaError.Types.NOT_FOUND) {
|
||||||
const config = this.config_.hashConfig ?? { logN: 15, r: 8, p: 1 }
|
|
||||||
const passwordHash = await Scrypt.kdf(password, config)
|
|
||||||
|
|
||||||
const createdAuthIdentity = await authIdentityService.create({
|
|
||||||
entity_id: email,
|
|
||||||
provider_metadata: {
|
|
||||||
password: passwordHash.toString("base64"),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const copy = JSON.parse(JSON.stringify(createdAuthIdentity))
|
|
||||||
const providerIdentity = copy.provider_identities?.find(
|
|
||||||
(pi) => pi.provider === this.provider
|
|
||||||
)!
|
|
||||||
delete providerIdentity.provider_metadata?.password
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: false,
|
||||||
authIdentity: copy,
|
error: "Invalid email or password",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,4 +120,51 @@ export class EmailPassAuthService extends AbstractAuthModuleProvider {
|
|||||||
error: "Invalid email or password",
|
error: "Invalid email or password",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async register(
|
||||||
|
userData: AuthenticationInput,
|
||||||
|
authIdentityService: AuthIdentityProviderService
|
||||||
|
): Promise<AuthenticationResponse> {
|
||||||
|
const { email, password } = userData.body ?? {}
|
||||||
|
|
||||||
|
if (!password || !isString(password)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Password should be a string",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email || !isString(email)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Email should be a string",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await authIdentityService.retrieve({
|
||||||
|
entity_id: email,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Identity with email already exists",
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.type === MedusaError.Types.NOT_FOUND) {
|
||||||
|
const createdAuthIdentity = await this.createAuthIdentity({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
authIdentityService,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
authIdentity: createdAuthIdentity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: error.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
Logger,
|
|
||||||
GoogleAuthProviderOptions,
|
|
||||||
AuthenticationResponse,
|
|
||||||
AuthenticationInput,
|
AuthenticationInput,
|
||||||
|
AuthenticationResponse,
|
||||||
AuthIdentityProviderService,
|
AuthIdentityProviderService,
|
||||||
|
GoogleAuthProviderOptions,
|
||||||
|
Logger,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import { AbstractAuthModuleProvider, MedusaError } from "@medusajs/utils"
|
import { AbstractAuthModuleProvider, MedusaError } from "@medusajs/utils"
|
||||||
import jwt, { JwtPayload } from "jsonwebtoken"
|
import jwt, { JwtPayload } from "jsonwebtoken"
|
||||||
@@ -29,6 +29,13 @@ export class GoogleAuthService extends AbstractAuthModuleProvider {
|
|||||||
this.logger_ = logger
|
this.logger_ = logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async register(_): Promise<AuthenticationResponse> {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
|
"Google does not support registration. Use method `authenticate` instead."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async authenticate(
|
async authenticate(
|
||||||
req: AuthenticationInput
|
req: AuthenticationInput
|
||||||
): Promise<AuthenticationResponse> {
|
): Promise<AuthenticationResponse> {
|
||||||
|
|||||||
Reference in New Issue
Block a user