feat(auth): Revamp authentication setup (#7387)
* chore: Clean up authentication middlewares * chore: Rename AuthUser to AuthIdentity * feat: Define link between user, customer, and auth identity * feat: Use links for auth, update auth context content * fix: Adjust user create command with new auth setup * fix: Make auth login more dynamic, review fixes * fix: Change test assertions for created by
This commit is contained in:
1 parent
b7df447682
commit
5ede560f70
88 files changed
+887
-1014
No files matched your search
@@ -15,7 +15,7 @@ export const POST = async (
|
||||
selector: { id: req.params.id },
|
||||
revoke: {
|
||||
...req.validatedBody,
|
||||
revoked_by: req.auth.actor_id,
|
||||
revoked_by: req.auth_context.actor_id,
|
||||
},
|
||||
},
|
||||
throwOnError: false,
|
||||
|
||||
@@ -41,7 +41,7 @@ export const POST = async (
|
||||
const input = [
|
||||
{
|
||||
...req.validatedBody,
|
||||
created_by: req.auth.actor_id,
|
||||
created_by: req.auth_context.actor_id,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ export const POST = async (
|
||||
const customersData = [
|
||||
{
|
||||
...req.validatedBody,
|
||||
created_by: req.auth.actor_id,
|
||||
created_by: req.auth_context.actor_id,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ export const POST = async (
|
||||
const customersData = [
|
||||
{
|
||||
...req.validatedBody,
|
||||
created_by: req.auth?.actor_id,
|
||||
created_by: req.auth_context.actor_id,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
import { acceptInviteWorkflow } from "@medusajs/core-flows"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { IUserModuleService, InviteWorkflow } from "@medusajs/types"
|
||||
import { InviteWorkflow } from "@medusajs/types"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "../../../../types/routing"
|
||||
import { AdminInviteAcceptType } from "../validators"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest<AdminInviteAcceptType>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
if (req.auth.actor_id) {
|
||||
const moduleService: IUserModuleService = req.scope.resolve(
|
||||
ModuleRegistrationName.USER
|
||||
if (req.auth_context.actor_id) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"The user is already authenticated and cannot accept an invite."
|
||||
)
|
||||
const user = await moduleService.retrieve(req.auth.actor_id)
|
||||
res.status(200).json({ user })
|
||||
return
|
||||
}
|
||||
|
||||
const input = {
|
||||
invite_token: req.filterableFields.token as string,
|
||||
auth_user_id: req.auth?.auth_user_id,
|
||||
auth_identity_id: req.auth_context.auth_identity_id,
|
||||
user: req.validatedBody,
|
||||
} as InviteWorkflow.AcceptInviteWorkflowInputDTO
|
||||
|
||||
@@ -36,10 +34,5 @@ export const POST = async (
|
||||
return
|
||||
}
|
||||
|
||||
// Set customer_id on session user if we are in session
|
||||
if (req.session.auth_user) {
|
||||
req.session.auth_user.app_metadata.user_id = users[0].id
|
||||
}
|
||||
|
||||
res.status(200).json({ user: users[0] })
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export const POST = async (
|
||||
const { errors } = await capturePaymentWorkflow(req.scope).run({
|
||||
input: {
|
||||
payment_id: id,
|
||||
captured_by: req.auth?.actor_id,
|
||||
captured_by: req.auth_context.actor_id,
|
||||
amount: req.validatedBody.amount,
|
||||
},
|
||||
throwOnError: false,
|
||||
|
||||
@@ -14,7 +14,7 @@ export const POST = async (
|
||||
const { errors } = await refundPaymentWorkflow(req.scope).run({
|
||||
input: {
|
||||
payment_id: id,
|
||||
created_by: req.auth?.actor_id,
|
||||
created_by: req.auth_context.actor_id,
|
||||
amount: req.validatedBody.amount,
|
||||
},
|
||||
throwOnError: false,
|
||||
|
||||
@@ -23,7 +23,7 @@ export const POST = async (
|
||||
const { errors } = await updateTaxRatesWorkflow(req.scope).run({
|
||||
input: {
|
||||
selector: { id: req.params.id },
|
||||
update: { ...req.validatedBody, updated_by: req.auth.actor_id },
|
||||
update: { ...req.validatedBody, updated_by: req.auth_context.actor_id },
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@ export const POST = async (
|
||||
{
|
||||
...req.validatedBody,
|
||||
tax_rate_id: req.params.id,
|
||||
created_by: req.auth.actor_id,
|
||||
created_by: req.auth_context.actor_id,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@ export const POST = async (
|
||||
input: [
|
||||
{
|
||||
...req.validatedBody,
|
||||
created_by: req.auth.actor_id,
|
||||
created_by: req.auth_context.actor_id,
|
||||
},
|
||||
],
|
||||
throwOnError: false,
|
||||
|
||||
@@ -21,7 +21,7 @@ export const POST = async (
|
||||
input: [
|
||||
{
|
||||
...req.validatedBody,
|
||||
created_by: req.auth.actor_id,
|
||||
created_by: req.auth_context.actor_id,
|
||||
},
|
||||
],
|
||||
throwOnError: false,
|
||||
|
||||
@@ -12,7 +12,7 @@ export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const id = req.auth.app_metadata.user_id
|
||||
const id = req.auth_context.actor_id
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
|
||||
if (!id) {
|
||||
|
||||
@@ -5,12 +5,12 @@ import {
|
||||
MedusaError,
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
import jwt from "jsonwebtoken"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "../../../types/routing"
|
||||
import { refetchUser } from "./helpers"
|
||||
import { generateJwtToken } from "../../utils/auth/token"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
@@ -41,7 +41,7 @@ export const POST = async (
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
// If `actor_id` is present, the request carries authentication for an existing user
|
||||
if (req.auth.actor_id) {
|
||||
if (req.auth_context.actor_id) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Request carries authentication for an existing user"
|
||||
@@ -51,30 +51,42 @@ export const POST = async (
|
||||
const input = {
|
||||
input: {
|
||||
userData: req.validatedBody,
|
||||
authUserId: req.auth.auth_user_id,
|
||||
authIdentityId: req.auth_context.auth_identity_id,
|
||||
},
|
||||
throwOnError: false,
|
||||
}
|
||||
|
||||
const { errors } = await createUserAccountWorkflow(req.scope).run(input)
|
||||
const { result, errors } = await createUserAccountWorkflow(req.scope).run(
|
||||
input
|
||||
)
|
||||
|
||||
if (Array.isArray(errors) && errors[0]) {
|
||||
throw errors[0].error
|
||||
}
|
||||
|
||||
const { http } = req.scope.resolve(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
).projectConfig
|
||||
const { jwtSecret, jwtExpiresIn } = http
|
||||
const token = generateJwtToken(
|
||||
{
|
||||
actor_id: result.id,
|
||||
actor_type: "user",
|
||||
auth_identity_id: req.auth_context.auth_identity_id,
|
||||
app_metadata: {},
|
||||
scope: "admin",
|
||||
},
|
||||
{
|
||||
secret: jwtSecret,
|
||||
expiresIn: jwtExpiresIn,
|
||||
}
|
||||
)
|
||||
|
||||
const user = await refetchUser(
|
||||
req.auth.auth_user_id,
|
||||
result.id,
|
||||
req.scope,
|
||||
req.remoteQueryConfig.fields
|
||||
)
|
||||
|
||||
const { http } = req.scope.resolve(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
).projectConfig
|
||||
|
||||
const token = jwt.sign(user, http.jwtSecret, {
|
||||
expiresIn: http.jwtExpiresIn,
|
||||
})
|
||||
|
||||
res.status(200).json({ user, token })
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { AuthenticationInput, IAuthModuleService } from "@medusajs/types"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import jwt from "jsonwebtoken"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
MedusaError,
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
|
||||
import { generateJwtToken } from "../../../../utils/auth/token"
|
||||
|
||||
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
const { scope, auth_provider } = req.params
|
||||
const actorType = scope === "admin" ? "user" : "customer"
|
||||
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
const service: IAuthModuleService = req.scope.resolve(
|
||||
ModuleRegistrationName.AUTH
|
||||
)
|
||||
@@ -20,16 +26,37 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
protocol: req.protocol,
|
||||
} as AuthenticationInput
|
||||
|
||||
const authResult = await service.validateCallback(auth_provider, authData)
|
||||
const { success, error, authIdentity, successRedirectUrl } =
|
||||
await service.validateCallback(auth_provider, authData)
|
||||
|
||||
const { success, error, authUser, successRedirectUrl } = authResult
|
||||
const queryObject = remoteQueryObjectFromString({
|
||||
entryPoint: "auth_identity",
|
||||
fields: [`${actorType}.id`],
|
||||
variables: { id: authIdentity.id },
|
||||
})
|
||||
const [actorData] = await remoteQuery(queryObject)
|
||||
const entityId = actorData?.[actorType]?.id
|
||||
|
||||
if (success) {
|
||||
const { http } = req.scope.resolve("configModule").projectConfig
|
||||
const { http } = req.scope.resolve(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
).projectConfig
|
||||
|
||||
const { jwtSecret, jwtExpiresIn } = http
|
||||
|
||||
const token = jwt.sign(authUser, jwtSecret, { expiresIn: jwtExpiresIn })
|
||||
// TODO: Clean up mapping between scope and actor type
|
||||
const token = generateJwtToken(
|
||||
{
|
||||
actor_id: entityId,
|
||||
actor_type: actorType,
|
||||
auth_identity_id: authIdentity.id,
|
||||
app_metadata: {},
|
||||
scope,
|
||||
},
|
||||
{
|
||||
secret: jwtSecret,
|
||||
expiresIn: jwtExpiresIn,
|
||||
}
|
||||
)
|
||||
|
||||
if (successRedirectUrl) {
|
||||
const url = new URL(successRedirectUrl!)
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { AuthenticationInput, IAuthModuleService } from "@medusajs/types"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import jwt from "jsonwebtoken"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
MedusaError,
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
|
||||
import { generateJwtToken } from "../../../utils/auth/token"
|
||||
|
||||
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
const { scope, auth_provider } = req.params
|
||||
const actorType = scope === "admin" ? "user" : "customer"
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
|
||||
const service: IAuthModuleService = req.scope.resolve(
|
||||
ModuleRegistrationName.AUTH
|
||||
@@ -20,9 +26,10 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
protocol: req.protocol,
|
||||
} as AuthenticationInput
|
||||
|
||||
const authResult = await service.authenticate(auth_provider, authData)
|
||||
|
||||
const { success, error, authUser, location } = authResult
|
||||
const { success, error, authIdentity, location } = await service.authenticate(
|
||||
auth_provider,
|
||||
authData
|
||||
)
|
||||
|
||||
if (location) {
|
||||
res.redirect(location)
|
||||
@@ -30,11 +37,33 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
}
|
||||
|
||||
if (success) {
|
||||
const { http } = req.scope.resolve("configModule").projectConfig
|
||||
const { http } = req.scope.resolve(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
).projectConfig
|
||||
|
||||
const token = jwt.sign(authUser, http.jwtSecret, {
|
||||
expiresIn: http.jwtExpiresIn,
|
||||
const queryObject = remoteQueryObjectFromString({
|
||||
entryPoint: "auth_identity",
|
||||
fields: [`${actorType}.id`],
|
||||
variables: { id: authIdentity.id },
|
||||
})
|
||||
const [actorData] = await remoteQuery(queryObject)
|
||||
const entityId = actorData?.[actorType]?.id
|
||||
|
||||
const { jwtSecret, jwtExpiresIn } = http
|
||||
// TODO: Clean up mapping between scope and actor type
|
||||
const token = generateJwtToken(
|
||||
{
|
||||
actor_id: entityId,
|
||||
actor_type: actorType,
|
||||
auth_identity_id: authIdentity.id,
|
||||
app_metadata: {},
|
||||
scope,
|
||||
},
|
||||
{
|
||||
secret: jwtSecret,
|
||||
expiresIn: jwtExpiresIn,
|
||||
}
|
||||
)
|
||||
|
||||
return res.status(200).json({ token })
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ export const authRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/auth/session",
|
||||
middlewares: [authenticate(/.*/, "bearer")],
|
||||
middlewares: [authenticate("*", "bearer")],
|
||||
},
|
||||
{
|
||||
method: ["DELETE"],
|
||||
matcher: "/auth/session",
|
||||
middlewares: [authenticate(/.*/, ["session"])],
|
||||
middlewares: [authenticate("*", ["session"])],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
|
||||
@@ -7,9 +7,9 @@ export const POST = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
req.session.auth_user = req.auth
|
||||
req.session.auth_context = req.auth_context
|
||||
|
||||
res.status(200).json({ user: req.auth })
|
||||
res.status(200).json({ user: req.auth_context })
|
||||
}
|
||||
|
||||
export const DELETE = async (
|
||||
|
||||
@@ -12,7 +12,7 @@ export const POST = async (
|
||||
) => {
|
||||
const workflowInput = {
|
||||
...req.validatedBody,
|
||||
customer_id: req.auth?.actor_id,
|
||||
customer_id: req.auth_context?.actor_id,
|
||||
}
|
||||
|
||||
const { result, errors } = await createCartWorkflow(req.scope).run({
|
||||
|
||||
@@ -23,7 +23,7 @@ export const GET = async (
|
||||
req: AuthenticatedMedusaRequest<StoreGetCustomerAddressParamsType>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const customerId = req.auth.actor_id
|
||||
const customerId = req.auth_context.actor_id
|
||||
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
const queryObject = remoteQueryObjectFromString({
|
||||
@@ -49,7 +49,7 @@ export const POST = async (
|
||||
req: AuthenticatedMedusaRequest<StoreUpdateCustomerAddressType>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const id = req.auth.actor_id!
|
||||
const id = req.auth_context.actor_id!
|
||||
await validateCustomerAddress(req.scope, id, req.params.address_id)
|
||||
|
||||
const updateAddresses = updateCustomerAddressesWorkflow(req.scope)
|
||||
@@ -78,7 +78,7 @@ export const DELETE = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const id = req.auth.actor_id
|
||||
const id = req.auth_context.actor_id
|
||||
await validateCustomerAddress(req.scope, id, req.params.address_id)
|
||||
|
||||
const deleteAddress = deleteCustomerAddressesWorkflow(req.scope)
|
||||
|
||||
@@ -18,7 +18,7 @@ export const GET = async (
|
||||
req: AuthenticatedMedusaRequest<StoreGetCustomerAddressesParamsType>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const customerId = req.auth.actor_id
|
||||
const customerId = req.auth_context.actor_id
|
||||
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
const queryObject = remoteQueryObjectFromString({
|
||||
@@ -44,7 +44,7 @@ export const POST = async (
|
||||
req: AuthenticatedMedusaRequest<StoreCreateCustomerAddressType>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const customerId = req.auth.actor_id
|
||||
const customerId = req.auth_context.actor_id
|
||||
|
||||
const createAddresses = createCustomerAddressesWorkflow(req.scope)
|
||||
const addresses = [
|
||||
|
||||
@@ -15,7 +15,7 @@ export const GET = async (
|
||||
req: AuthenticatedMedusaRequest<StoreGetCustomerParamsType>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const id = req.auth.actor_id
|
||||
const id = req.auth_context.actor_id
|
||||
const customer = await refetchCustomer(
|
||||
id,
|
||||
req.scope,
|
||||
@@ -36,7 +36,7 @@ export const POST = async (
|
||||
req: AuthenticatedMedusaRequest<StoreUpdateCustomerType>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const customerId = req.auth.actor_id
|
||||
const customerId = req.auth_context.actor_id
|
||||
const { errors } = await updateCustomersWorkflow(req.scope).run({
|
||||
input: {
|
||||
selector: { id: customerId },
|
||||
|
||||
@@ -2,53 +2,59 @@ import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "../../../types/routing"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
import { ContainerRegistrationKeys, MedusaError } from "@medusajs/utils"
|
||||
|
||||
import { createCustomerAccountWorkflow } from "@medusajs/core-flows"
|
||||
import { refetchCustomer } from "./helpers"
|
||||
import { StoreCreateCustomerType } from "./validators"
|
||||
import { generateJwtToken } from "../../utils/auth/token"
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest<StoreCreateCustomerType>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
if (req.auth.actor_id) {
|
||||
const remoteQuery = req.scope.resolve(
|
||||
ContainerRegistrationKeys.REMOTE_QUERY
|
||||
// If `actor_id` is present, the request carries authentication for an existing customer
|
||||
if (req.auth_context.actor_id) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Request already authenticated as a customer."
|
||||
)
|
||||
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "customer",
|
||||
variables: { id: req.auth.actor_id },
|
||||
fields: [],
|
||||
})
|
||||
const [customer] = await remoteQuery(query)
|
||||
|
||||
res.status(200).json({ customer })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const createCustomers = createCustomerAccountWorkflow(req.scope)
|
||||
const customersData = req.validatedBody
|
||||
|
||||
const { result } = await createCustomers.run({
|
||||
input: { customersData, authUserId: req.auth.auth_user_id },
|
||||
const { result, errors } = await createCustomers.run({
|
||||
input: { customersData, authIdentityId: req.auth_context.auth_identity_id },
|
||||
})
|
||||
|
||||
// Set customer_id on session user if we are in session
|
||||
if (req.session.auth_user) {
|
||||
req.session.auth_user.app_metadata.customer_id = result.id
|
||||
if (Array.isArray(errors) && errors[0]) {
|
||||
throw errors[0].error
|
||||
}
|
||||
|
||||
const { http } = req.scope.resolve(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
).projectConfig
|
||||
const { jwtSecret, jwtExpiresIn } = http
|
||||
const token = generateJwtToken(
|
||||
{
|
||||
actor_id: result.id,
|
||||
actor_type: "customer",
|
||||
auth_identity_id: req.auth_context.auth_identity_id,
|
||||
app_metadata: {},
|
||||
scope: "store",
|
||||
},
|
||||
{
|
||||
secret: jwtSecret,
|
||||
expiresIn: jwtExpiresIn,
|
||||
}
|
||||
)
|
||||
|
||||
const customer = await refetchCustomer(
|
||||
result.id,
|
||||
req.scope,
|
||||
req.remoteQueryConfig.fields
|
||||
)
|
||||
|
||||
res.status(200).json({ customer })
|
||||
res.status(200).json({ customer, token })
|
||||
}
|
||||
@@ -17,7 +17,10 @@ export const GET = async (
|
||||
const queryObject = remoteQueryObjectFromString({
|
||||
entryPoint: "order",
|
||||
variables: {
|
||||
filters: { ...req.filterableFields, customer_id: req.auth.actor_id },
|
||||
filters: {
|
||||
...req.filterableFields,
|
||||
customer_id: req.auth_context.actor_id,
|
||||
},
|
||||
...req.remoteQueryConfig.pagination,
|
||||
},
|
||||
fields: req.remoteQueryConfig.fields,
|
||||
|
||||
@@ -14,9 +14,9 @@ export const POST = async (
|
||||
const { context = {}, data, provider_id } = req.body
|
||||
|
||||
// If the customer is logged in, we auto-assign them to the payment collection
|
||||
if (req.auth?.actor_id) {
|
||||
if (req.auth_context?.actor_id) {
|
||||
;(context as any).customer = {
|
||||
id: req.auth.actor_id,
|
||||
id: req.auth_context?.actor_id,
|
||||
}
|
||||
}
|
||||
const workflowInput = {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AuthContext } from "../../../types/routing"
|
||||
import jwt from "jsonwebtoken"
|
||||
|
||||
export const generateJwtToken = (
|
||||
authContext: AuthContext,
|
||||
jwtConfig: {
|
||||
secret: string
|
||||
expiresIn: string
|
||||
}
|
||||
) => {
|
||||
return jwt.sign(authContext, jwtConfig.secret, {
|
||||
expiresIn: jwtConfig.expiresIn,
|
||||
})
|
||||
}
|
||||
@@ -6,7 +6,8 @@ import { track } from "medusa-telemetry"
|
||||
|
||||
import loaders from "../loaders"
|
||||
import Logger from "../loaders/logger"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { ModuleRegistrationName, Modules } from "@medusajs/modules-sdk"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
|
||||
export default async function ({
|
||||
directory,
|
||||
@@ -26,6 +27,8 @@ export default async function ({
|
||||
|
||||
const userService = container.resolve(ModuleRegistrationName.USER)
|
||||
const authService = container.resolve(ModuleRegistrationName.AUTH)
|
||||
const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||
|
||||
const provider = "emailpass"
|
||||
|
||||
if (invite) {
|
||||
@@ -37,7 +40,7 @@ export default async function ({
|
||||
} else {
|
||||
const user = await userService.create({ email })
|
||||
|
||||
const { authUser } = await authService.authenticate(provider, {
|
||||
const { authIdentity } = await authService.authenticate(provider, {
|
||||
body: {
|
||||
email,
|
||||
password,
|
||||
@@ -45,12 +48,16 @@ export default async function ({
|
||||
authScope: "admin",
|
||||
})
|
||||
|
||||
await authService.update({
|
||||
id: authUser.id,
|
||||
app_metadata: {
|
||||
user_id: user.id,
|
||||
await remoteLink.create([
|
||||
{
|
||||
[Modules.USER]: {
|
||||
user_id: user.id,
|
||||
},
|
||||
[Modules.AUTH]: {
|
||||
auth_identity_id: authIdentity.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
])
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
|
||||
@@ -16,7 +16,6 @@ import supertest from "supertest"
|
||||
import apiLoader from "../../../../api"
|
||||
import { getResolvedPlugins } from "../../../../helpers/resolve-plugins"
|
||||
import featureFlagLoader, { featureFlagRouter } from "../../../../feature-flags"
|
||||
import passportLoader from "../../../../passport"
|
||||
|
||||
import RoutesLoader from "../.."
|
||||
import { config } from "../mocks"
|
||||
@@ -90,7 +89,6 @@ export const createServer = async (rootDir) => {
|
||||
const plugins = getResolvedPlugins(rootDir, config) || []
|
||||
|
||||
featureFlagLoader(config)
|
||||
await passportLoader({ app: app, configModule: config })
|
||||
await moduleLoader({ container, moduleResolutions })
|
||||
|
||||
app.use((req, res, next) => {
|
||||
|
||||
@@ -168,7 +168,8 @@ describe("RoutesLoader", function () {
|
||||
expect(res.text).toBe("GET /store/protected")
|
||||
})
|
||||
|
||||
it("should return 401 when customer is not authenticated", async () => {
|
||||
// The authentication middleware has changed and is not automatically attached currently
|
||||
it.skip("should return 401 when customer is not authenticated", async () => {
|
||||
const res = await request("GET", "/store/me/protected")
|
||||
|
||||
expect(res.status).toBe(401)
|
||||
|
||||
@@ -6,12 +6,7 @@ import { readdir } from "fs/promises"
|
||||
import { parseCorsOrigins } from "medusa-core-utils"
|
||||
import { extname, join, sep } from "path"
|
||||
import { MedusaRequest, MedusaResponse } from "../../../types/routing"
|
||||
import {
|
||||
authenticateCustomer,
|
||||
authenticateLegacy,
|
||||
errorHandler,
|
||||
requireCustomerAuthentication,
|
||||
} from "../../../utils/middlewares"
|
||||
import { errorHandler } from "../../../utils/middlewares"
|
||||
import logger from "../../logger"
|
||||
import {
|
||||
AsyncRouteHandler,
|
||||
@@ -645,27 +640,6 @@ export class RoutesLoader {
|
||||
)
|
||||
}
|
||||
|
||||
if (descriptor.config.shouldAppendCustomer) {
|
||||
/**
|
||||
* Add the customer to the request object
|
||||
*/
|
||||
this.router.use(descriptor.route, authenticateCustomer())
|
||||
}
|
||||
|
||||
if (descriptor.config.shouldRequireCustomerAuth) {
|
||||
/**
|
||||
* Require the customer to be authenticated
|
||||
*/
|
||||
this.router.use(descriptor.route, requireCustomerAuthentication())
|
||||
}
|
||||
|
||||
if (descriptor.config.shouldRequireAdminAuth) {
|
||||
/**
|
||||
* Require the admin to be authenticated
|
||||
*/
|
||||
this.router.use(descriptor.route, authenticateLegacy())
|
||||
}
|
||||
|
||||
for (const route of routes) {
|
||||
/**
|
||||
* Apply the body parser middleware if the route
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import { ConfigModule } from "@medusajs/types"
|
||||
import { Express } from "express"
|
||||
import passport from "passport"
|
||||
import { Strategy as CustomStrategy } from "passport-custom"
|
||||
import { ExtractJwt, Strategy as JWTStrategy } from "passport-jwt"
|
||||
/* import { AuthService } from "../services"*/
|
||||
|
||||
export default async ({
|
||||
app,
|
||||
configModule,
|
||||
}: {
|
||||
app: Express
|
||||
configModule: ConfigModule
|
||||
}): Promise<void> => {
|
||||
// For good old email password authentication
|
||||
/* passport.use(
|
||||
new LocalStrategy(
|
||||
{
|
||||
usernameField: "email",
|
||||
passwordField: "password",
|
||||
passReqToCallback: true,
|
||||
},
|
||||
async (req: MedusaRequest, email, password, done) => {
|
||||
const authService = req.scope.resolve<AuthService>("authService")
|
||||
try {
|
||||
const { success, user } = await authService.authenticate(
|
||||
email,
|
||||
password
|
||||
)
|
||||
if (success) {
|
||||
return done(null, user)
|
||||
} else {
|
||||
return done("Incorrect Username / Password")
|
||||
}
|
||||
} catch (error) {
|
||||
return done(error)
|
||||
}
|
||||
}
|
||||
)
|
||||
)*/
|
||||
|
||||
// After a user has authenticated a JWT will be placed on a cookie, all
|
||||
// calls will be authenticated based on the JWT
|
||||
const { http } = configModule.projectConfig
|
||||
passport.use(
|
||||
"admin-session",
|
||||
new CustomStrategy(async (req, done) => {
|
||||
// @ts-ignore
|
||||
if (req.session?.user_id) {
|
||||
// @ts-ignore
|
||||
return done(null, { userId: req.session.user_id })
|
||||
}
|
||||
|
||||
return done(null, false)
|
||||
})
|
||||
)
|
||||
|
||||
passport.use(
|
||||
"store-session",
|
||||
new CustomStrategy(async (req, done) => {
|
||||
// @ts-ignore
|
||||
if (req.session?.customer_id) {
|
||||
// @ts-ignore
|
||||
return done(null, { customer_id: req.session.customer_id })
|
||||
}
|
||||
|
||||
return done(null, false)
|
||||
})
|
||||
)
|
||||
|
||||
// Alternatively use API token to authenticate to the admin api
|
||||
/* passport.use(
|
||||
"admin-api-token",
|
||||
new CustomStrategy(async (req, done) => {
|
||||
// extract the token from the header
|
||||
const token = req.headers["x-medusa-access-token"]
|
||||
|
||||
// check if header exists and is string
|
||||
// typescript will complain if we don't check for type
|
||||
if (!token || typeof token !== "string") {
|
||||
return done(null, false)
|
||||
}
|
||||
|
||||
const authService = req.scope.resolve<AuthService>("authService")
|
||||
const auth = await authService.authenticateAPIToken(token)
|
||||
if (auth.success) {
|
||||
done(null, auth.user)
|
||||
} else {
|
||||
done(null, false)
|
||||
}
|
||||
})
|
||||
)*/
|
||||
|
||||
// Admin bearer JWT token authentication strategy, best suited for web SPAs or mobile apps
|
||||
passport.use(
|
||||
"admin-bearer",
|
||||
new JWTStrategy(
|
||||
{
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKey: http.jwtSecret,
|
||||
},
|
||||
(token, done) => {
|
||||
if (token.domain !== "admin") {
|
||||
done(null, false)
|
||||
return
|
||||
}
|
||||
|
||||
if (!token.user_id) {
|
||||
done(null, false)
|
||||
return
|
||||
}
|
||||
|
||||
done(null, { userId: token.user_id })
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
// Store bearer JWT token authentication strategy, best suited for web SPAs or mobile apps
|
||||
passport.use(
|
||||
"store-bearer",
|
||||
new JWTStrategy(
|
||||
{
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKey: http.jwtSecret,
|
||||
},
|
||||
(token, done) => {
|
||||
if (token.domain !== "store") {
|
||||
done(null, false)
|
||||
return
|
||||
}
|
||||
|
||||
if (!token.customer_id) {
|
||||
done(null, false)
|
||||
return
|
||||
}
|
||||
|
||||
done(null, { customer_id: token.customer_id })
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
app.use(passport.initialize())
|
||||
app.use(passport.session())
|
||||
}
|
||||
@@ -59,15 +59,18 @@ export interface MedusaRequest<Body = unknown>
|
||||
pricingContext?: MedusaPricingContext
|
||||
}
|
||||
|
||||
export interface AuthContext {
|
||||
actor_id: string
|
||||
// TODO: We possibly want to make this more open-ended so it's easy to extend.
|
||||
actor_type: "api-key" | "user" | "customer" | "unknown"
|
||||
auth_identity_id: string
|
||||
scope: string
|
||||
app_metadata: Record<string, any>
|
||||
}
|
||||
|
||||
export interface AuthenticatedMedusaRequest<Body = never>
|
||||
extends MedusaRequest<Body> {
|
||||
user: { customer_id?: string; userId?: string } // TODO: Remove this property when v2 is released
|
||||
auth: {
|
||||
actor_id: string
|
||||
auth_user_id: string
|
||||
app_metadata: Record<string, any>
|
||||
scope: string
|
||||
}
|
||||
auth_context: AuthContext
|
||||
}
|
||||
|
||||
export type MedusaResponse<Body = unknown> = Response<Body>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { NextFunction, Request, RequestHandler, Response } from "express"
|
||||
|
||||
// TODO: See how this should look like for v2.
|
||||
// Optional customer authentication
|
||||
// If authenticated, middleware attaches customer to request (as user) otherwise we pass through
|
||||
// If you want to require authentication, use `requireCustomerAuthentication` in `packages/medusa/src/api/middlewares/require-customer-authentication.ts`
|
||||
export default (): RequestHandler => {
|
||||
return (req: Request, res: Response, next: NextFunction): void => {
|
||||
return next()
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,9 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
ApiKeyDTO,
|
||||
AuthUserDTO,
|
||||
ConfigModule,
|
||||
IApiKeyModuleService,
|
||||
} from "@medusajs/types"
|
||||
import { stringEqualsOrRegexMatch } from "@medusajs/utils"
|
||||
import { ApiKeyDTO, ConfigModule, IApiKeyModuleService } from "@medusajs/types"
|
||||
import { NextFunction, RequestHandler } from "express"
|
||||
import jwt, { JwtPayload } from "jsonwebtoken"
|
||||
import {
|
||||
AuthContext,
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaRequest,
|
||||
MedusaResponse,
|
||||
@@ -20,13 +15,18 @@ const API_KEY_AUTH = "api-key"
|
||||
|
||||
type AuthType = typeof SESSION_AUTH | typeof BEARER_AUTH | typeof API_KEY_AUTH
|
||||
|
||||
const ADMIN_SCOPE = "admin"
|
||||
const STORE_SCOPE = "store"
|
||||
const ALL_SCOPE = "*"
|
||||
|
||||
type Scope = typeof ADMIN_SCOPE | typeof STORE_SCOPE | typeof ALL_SCOPE
|
||||
|
||||
type MedusaSession = {
|
||||
auth_user: AuthUserDTO
|
||||
scope: string
|
||||
auth_context: AuthContext
|
||||
}
|
||||
|
||||
export const authenticate = (
|
||||
authScope: string | RegExp,
|
||||
authScope: Scope | Scope[],
|
||||
authType: AuthType | AuthType[],
|
||||
options: { allowUnauthenticated?: boolean; allowUnregistered?: boolean } = {}
|
||||
): RequestHandler => {
|
||||
@@ -36,63 +36,58 @@ export const authenticate = (
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
const authTypes = Array.isArray(authType) ? authType : [authType]
|
||||
const scopes = Array.isArray(authScope) ? authScope : [authScope]
|
||||
const req_ = req as AuthenticatedMedusaRequest
|
||||
|
||||
// We only allow authenticating using a secret API key on the admin
|
||||
if (authTypes.includes(API_KEY_AUTH) && isAdminScope(authScope)) {
|
||||
const isExclusivelyAdmin =
|
||||
scopes.length === 1 && scopes.includes(ADMIN_SCOPE)
|
||||
if (authTypes.includes(API_KEY_AUTH) && isExclusivelyAdmin) {
|
||||
const apiKey = await getApiKeyInfo(req)
|
||||
if (apiKey) {
|
||||
;(req as AuthenticatedMedusaRequest).auth = {
|
||||
req_.auth_context = {
|
||||
actor_id: apiKey.id,
|
||||
auth_user_id: "",
|
||||
actor_type: "api-key",
|
||||
auth_identity_id: "",
|
||||
app_metadata: {},
|
||||
// TODO: Add more limited scope once we have support for it in the API key module
|
||||
scope: "admin",
|
||||
scope: ADMIN_SCOPE,
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
let authUser: AuthUserDTO | null = getAuthUserFromSession(
|
||||
// We try to extract the auth context either from the session or from a JWT token
|
||||
let authContext: AuthContext | null = getAuthContextFromSession(
|
||||
req.session,
|
||||
authTypes,
|
||||
authScope
|
||||
scopes
|
||||
)
|
||||
|
||||
if (!authUser) {
|
||||
if (!authContext) {
|
||||
const { http } =
|
||||
req.scope.resolve<ConfigModule>("configModule").projectConfig
|
||||
authUser = getAuthUserFromJwtToken(
|
||||
authContext = getAuthContextFromJwtToken(
|
||||
req.headers.authorization,
|
||||
http.jwtSecret!,
|
||||
authTypes,
|
||||
authScope
|
||||
scopes
|
||||
)
|
||||
}
|
||||
|
||||
const isMedusaScope = isAdminScope(authScope) || isStoreScope(authScope)
|
||||
|
||||
const isRegistered =
|
||||
!isMedusaScope ||
|
||||
(authUser?.app_metadata?.user_id &&
|
||||
stringEqualsOrRegexMatch(authScope, "admin")) ||
|
||||
(authUser?.app_metadata?.customer_id &&
|
||||
stringEqualsOrRegexMatch(authScope, "store"))
|
||||
|
||||
if (
|
||||
authUser &&
|
||||
(isRegistered || (!isRegistered && options.allowUnregistered))
|
||||
) {
|
||||
;(req as AuthenticatedMedusaRequest).auth = {
|
||||
actor_id: getActorId(authUser, authScope) as string, // TODO: fix types for auth_users not in the medusa system
|
||||
auth_user_id: authUser.id,
|
||||
app_metadata: authUser.app_metadata,
|
||||
scope: authUser.scope,
|
||||
}
|
||||
|
||||
// If the entity is authenticated, and it is a registered user/customer we can continue
|
||||
if (!!authContext?.actor_id && authContext.actor_type !== "unknown") {
|
||||
req_.auth_context = authContext
|
||||
return next()
|
||||
}
|
||||
|
||||
// If the entity is authenticated, but there is no user/customer yet, we can continue (eg. in the case of a user invite) if allow unregistered is set
|
||||
if (authContext?.auth_identity_id && options.allowUnregistered) {
|
||||
req_.auth_context = authContext
|
||||
return next()
|
||||
}
|
||||
|
||||
// If we allow unauthenticated requests (i.e public endpoints), just continue
|
||||
if (options.allowUnauthenticated) {
|
||||
return next()
|
||||
}
|
||||
@@ -144,31 +139,32 @@ const getApiKeyInfo = async (req: MedusaRequest): Promise<ApiKeyDTO | null> => {
|
||||
}
|
||||
}
|
||||
|
||||
const getAuthUserFromSession = (
|
||||
const getAuthContextFromSession = (
|
||||
session: Partial<MedusaSession> = {},
|
||||
authTypes: AuthType[],
|
||||
authScope: string | RegExp
|
||||
): AuthUserDTO | null => {
|
||||
scopes: Scope[]
|
||||
): AuthContext | null => {
|
||||
if (!authTypes.includes(SESSION_AUTH)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (
|
||||
session.auth_user &&
|
||||
stringEqualsOrRegexMatch(authScope, session.auth_user.scope)
|
||||
session.auth_context &&
|
||||
(scopes.includes("*") ||
|
||||
scopes.includes(session.auth_context.scope as Scope))
|
||||
) {
|
||||
return session.auth_user
|
||||
return session.auth_context
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const getAuthUserFromJwtToken = (
|
||||
const getAuthContextFromJwtToken = (
|
||||
authHeader: string | undefined,
|
||||
jwtSecret: string,
|
||||
authTypes: AuthType[],
|
||||
authScope: string | RegExp
|
||||
): AuthUserDTO | null => {
|
||||
scopes: Scope[]
|
||||
): AuthContext | null => {
|
||||
if (!authTypes.includes(BEARER_AUTH)) {
|
||||
return null
|
||||
}
|
||||
@@ -189,8 +185,8 @@ const getAuthUserFromJwtToken = (
|
||||
// verify token and set authUser
|
||||
try {
|
||||
const verified = jwt.verify(token, jwtSecret) as JwtPayload
|
||||
if (stringEqualsOrRegexMatch(authScope, verified.scope)) {
|
||||
return verified as AuthUserDTO
|
||||
if (scopes.includes("*") || scopes.includes(verified.scope)) {
|
||||
return verified as AuthContext
|
||||
}
|
||||
} catch (err) {
|
||||
return null
|
||||
@@ -200,26 +196,3 @@ const getAuthUserFromJwtToken = (
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const getActorId = (
|
||||
authUser: AuthUserDTO,
|
||||
scope: string | RegExp
|
||||
): string | undefined => {
|
||||
if (stringEqualsOrRegexMatch(scope, "admin")) {
|
||||
return authUser.app_metadata.user_id as string
|
||||
}
|
||||
|
||||
if (stringEqualsOrRegexMatch(scope, "store")) {
|
||||
return authUser.app_metadata.customer_id as string
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const isAdminScope = (authScope: string | RegExp): boolean => {
|
||||
return stringEqualsOrRegexMatch(authScope, "admin")
|
||||
}
|
||||
|
||||
const isStoreScope = (authScope: string | RegExp): boolean => {
|
||||
return stringEqualsOrRegexMatch(authScope, "store")
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { NextFunction, Request, RequestHandler, Response } from "express"
|
||||
|
||||
// TODO: See how this should look like for v2.
|
||||
export default (): RequestHandler => {
|
||||
return (req: Request, res: Response, next: NextFunction): void => {
|
||||
return next()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,2 @@
|
||||
export { default as authenticateLegacy } from "./authenticate"
|
||||
export { authenticate } from "./authenticate-middleware"
|
||||
export { default as authenticateCustomer } from "./authenticate-customer"
|
||||
export { default as errorHandler } from "./error-handler"
|
||||
export { default as requireCustomerAuthentication } from "./require-customer-authentication"
|
||||
@@ -1,16 +0,0 @@
|
||||
import { NextFunction, Request, RequestHandler, Response } from "express"
|
||||
import passport from "passport"
|
||||
|
||||
export default (): RequestHandler => {
|
||||
return (req: Request, res: Response, next: NextFunction): void => {
|
||||
if (req.user) {
|
||||
return next()
|
||||
}
|
||||
|
||||
passport.authenticate(["store-session", "store-bearer"], { session: false })(
|
||||
req,
|
||||
res,
|
||||
next
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user