feat: add Medusa Cloud OAuth provider (#14395)

* feat: add Medusa Cloud OAuth provider

* add Cloud login button

* fetch whether cloud auth is enabled through api

* allow unregistered to get session

* handle existing users

* address PR comments

* prevent double execution

* a few more fixes

* fix callback url

* fix spelling

* refresh session

* 200 instead of 201

* only allow cloud identities to create user

* fix condition
This commit is contained in:
Pedro Guzman authored and GitHub committed 2025-12-30 17:30:10 +01:00
1 parent 499dec6d31
commit 001923da2b
27 files changed
+1327 -23

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@ export const authRoutesMiddlewares: MiddlewareRoute[] = [
{
method: ["POST"],
matcher: "/auth/session",
middlewares: [authenticate("*", "bearer")],
middlewares: [authenticate("*", "bearer", { allowUnregistered: true })],
},
{
method: ["DELETE"],
@@ -64,7 +64,12 @@ export const validateToken = () => {
provider: auth_provider,
},
{
select: ["provider_metadata", "auth_identity_id", "entity_id"],
select: [
"provider_metadata",
"auth_identity_id",
"entity_id",
"user_metadata",
],
}
)
@@ -77,6 +82,7 @@ export const validateToken = () => {
auth_identity_id: providerIdentity.auth_identity_id!,
actor_id: providerIdentity.entity_id,
app_metadata: {},
user_metadata: providerIdentity.user_metadata ?? {},
}
return next()
@@ -0,0 +1,11 @@
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const config = req.scope.resolve(ContainerRegistrationKeys.CONFIG_MODULE)
res.status(200).json({
enabled:
!!config.projectConfig.http.authMethodsPerActor?.user?.includes("cloud"),
})
}
@@ -0,0 +1,103 @@
import {
createUserAccountWorkflow,
setAuthAppMetadataWorkflow,
} from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import {
ContainerRegistrationKeys,
MedusaError,
} from "@medusajs/framework/utils"
export const POST = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
// If user already exists for this auth identity, reject
if (req.auth_context.actor_id) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"The user is already registered and cannot create a new account."
)
}
if (!req.auth_context.user_metadata.email) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Email is required to create a user account."
)
}
// Check that the auth identity is from Medusa Cloud
const providerIdentities = await query
.graph({
entity: "auth_identity",
fields: ["id", "provider_identities.provider"],
filters: {
id: req.auth_context.auth_identity_id,
},
})
.then((result) => result.data[0]?.provider_identities)
if (
providerIdentities?.length !== 1 ||
providerIdentities[0].provider !== "cloud"
) {
throw new MedusaError(
MedusaError.Types.UNAUTHORIZED,
"Only cloud identities can create a user account"
)
}
// Check if a user already exists
const user = await query
.graph({
entity: "user",
fields: ["id"],
filters: {
email: req.auth_context.user_metadata.email,
},
})
.then((result) => result.data[0])
if (user) {
await setAuthAppMetadataWorkflow(req.scope).run({
input: {
authIdentityId: req.auth_context.auth_identity_id,
actorType: "user",
value: user.id,
},
})
const updatedUser = await query
.graph({
entity: "user",
fields: ["*"],
filters: {
id: user.id,
},
})
.then((result) => result.data[0])
res.status(200).json({ user: updatedUser })
return
}
const { result: createdUser } = await createUserAccountWorkflow(
req.scope
).run({
input: {
authIdentityId: req.auth_context.auth_identity_id,
userData: {
email: req.auth_context.user_metadata.email as string,
first_name: req.auth_context.user_metadata.given_name as string,
last_name: req.auth_context.user_metadata.family_name as string,
},
},
})
res.status(200).json({ user: createdUser })
}
@@ -0,0 +1,19 @@
import { authenticate, MiddlewareRoute } from "@medusajs/framework/http"
export const cloudRoutesMiddlewares: MiddlewareRoute[] = [
{
matcher: "/cloud/auth",
method: ["GET"],
middlewares: [],
},
{
matcher: "/cloud/auth/users",
method: ["POST"],
middlewares: [
// Allow users who are authenticated but don't yet have an actor (user record)
authenticate("user", ["session", "bearer"], {
allowUnregistered: true,
}),
],
},
]
+8 -6
View File
@@ -37,16 +37,21 @@ import { adminShippingOptionRoutesMiddlewares } from "./admin/shipping-options/m
import { adminShippingProfilesMiddlewares } from "./admin/shipping-profiles/middlewares"
import { adminStockLocationRoutesMiddlewares } from "./admin/stock-locations/middlewares"
import { adminStoreRoutesMiddlewares } from "./admin/stores/middlewares"
import { adminTaxProviderRoutesMiddlewares } from "./admin/tax-providers/middlewares"
import { adminTaxRateRoutesMiddlewares } from "./admin/tax-rates/middlewares"
import { adminTaxRegionRoutesMiddlewares } from "./admin/tax-regions/middlewares"
import { adminTaxProviderRoutesMiddlewares } from "./admin/tax-providers/middlewares"
import { adminUploadRoutesMiddlewares } from "./admin/uploads/middlewares"
import { adminUserRoutesMiddlewares } from "./admin/users/middlewares"
import { viewConfigurationRoutesMiddlewares } from "./admin/views/[entity]/configurations/middlewares"
import { columnRoutesMiddlewares } from "./admin/views/[entity]/columns/middlewares"
import { viewConfigurationRoutesMiddlewares } from "./admin/views/[entity]/configurations/middlewares"
import { adminWorkflowsExecutionsMiddlewares } from "./admin/workflows-executions/middlewares"
import { authRoutesMiddlewares } from "./auth/middlewares"
import { adminIndexRoutesMiddlewares } from "./admin/index/middlewares"
import { adminLocalesRoutesMiddlewares } from "./admin/locales/middlewares"
import { adminShippingOptionTypeRoutesMiddlewares } from "./admin/shipping-option-types/middlewares"
import { adminTranslationsRoutesMiddlewares } from "./admin/translations/middlewares"
import { cloudRoutesMiddlewares } from "./cloud/middlewares"
import { hooksRoutesMiddlewares } from "./hooks/middlewares"
import { storeCartRoutesMiddlewares } from "./store/carts/middlewares"
import { storeCollectionRoutesMiddlewares } from "./store/collections/middlewares"
@@ -64,10 +69,6 @@ import { storeProductRoutesMiddlewares } from "./store/products/middlewares"
import { storeRegionRoutesMiddlewares } from "./store/regions/middlewares"
import { storeReturnReasonRoutesMiddlewares } from "./store/return-reasons/middlewares"
import { storeShippingOptionRoutesMiddlewares } from "./store/shipping-options/middlewares"
import { adminShippingOptionTypeRoutesMiddlewares } from "./admin/shipping-option-types/middlewares"
import { adminIndexRoutesMiddlewares } from "./admin/index/middlewares"
import { adminLocalesRoutesMiddlewares } from "./admin/locales/middlewares"
import { adminTranslationsRoutesMiddlewares } from "./admin/translations/middlewares"
export default defineMiddlewares([
...storeRoutesMiddlewares,
@@ -140,4 +141,5 @@ export default defineMiddlewares([
...viewConfigurationRoutesMiddlewares,
...columnRoutesMiddlewares,
...adminIndexRoutesMiddlewares,
...cloudRoutesMiddlewares,
])