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:
@@ -1 +1,2 @@
|
||||
export * from "./generate-reset-password-token"
|
||||
export * from "./set-auth-app-metadata"
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
createWorkflow,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import {
|
||||
setAuthAppMetadataStep,
|
||||
SetAuthAppMetadataStepInput,
|
||||
} from "../steps/set-auth-app-metadata"
|
||||
|
||||
export const setAuthAppMetadataWorkflowId = "set-auth-app-metadata-workflow"
|
||||
/**
|
||||
* This workflow sets the `app_metadata` property of an auth identity. This is useful to
|
||||
* associate a user (whether it's an admin user or customer) with an auth identity
|
||||
* that allows them to authenticate into Medusa.
|
||||
*
|
||||
* You can learn more about auth identites in
|
||||
* [this documentation](https://docs.medusajs.com/resources/commerce-modules/auth/auth-identity-and-actor-types).
|
||||
*
|
||||
* To use this for a custom actor type, check out [this guide](https://docs.medusajs.com/resources/commerce-modules/auth/create-actor-type)
|
||||
* that explains how to create a custom `manager` actor type and manage its users.
|
||||
*
|
||||
* @example
|
||||
* To associate an auth identity with an actor type (user, customer, or other actor types):
|
||||
*
|
||||
* ```ts
|
||||
* const { result } = await setAuthAppMetadataWorkflow(container).run({
|
||||
* input: {
|
||||
* authIdentityId: "au_1234",
|
||||
* actorType: "user", // or `customer`, or custom type
|
||||
* value: "user_123"
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* To remove the association with an actor type, such as when deleting the user:
|
||||
*
|
||||
* ```ts
|
||||
* const { result } = await setAuthAppMetadataWorkflow(container).run({
|
||||
* input: {
|
||||
* authIdentityId: "au_1234",
|
||||
* actorType: "user", // or `customer`, or custom type
|
||||
* value: null
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const setAuthAppMetadataWorkflow = createWorkflow(
|
||||
setAuthAppMetadataWorkflowId,
|
||||
(input: SetAuthAppMetadataStepInput) => {
|
||||
const authIdentity = setAuthAppMetadataStep(input)
|
||||
return new WorkflowResponse(authIdentity)
|
||||
}
|
||||
)
|
||||
@@ -58,6 +58,7 @@ export const authenticate = (
|
||||
actor_type: "api-key",
|
||||
auth_identity_id: "",
|
||||
app_metadata: {},
|
||||
user_metadata: {},
|
||||
}
|
||||
|
||||
return next()
|
||||
|
||||
@@ -198,6 +198,7 @@ export interface AuthContext {
|
||||
actor_type: string
|
||||
auth_identity_id: string
|
||||
app_metadata: Record<string, unknown>
|
||||
user_metadata: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface PublishableKeyContext {
|
||||
|
||||
@@ -262,6 +262,22 @@ export type MedusaCloudOptions = {
|
||||
* The endpoint of the Medusa Cloud email service.
|
||||
*/
|
||||
emailsEndpoint?: string
|
||||
/**
|
||||
* The authorization endpoint of the Medusa Cloud OAuth service.
|
||||
*/
|
||||
oauthAuthorizeEndpoint?: string
|
||||
/**
|
||||
* The token endpoint of the Medusa Cloud OAuth token service.
|
||||
*/
|
||||
oauthTokenEndpoint?: string
|
||||
/**
|
||||
* The callback URL for the Medusa Cloud OAuth service. If not provided, it will be set to `${AdminOptions.backendUrl}/auth/user/cloud/callback`.
|
||||
*/
|
||||
oauthCallbackUrl?: string
|
||||
/**
|
||||
* Whether the Medusa Cloud OAuth service is disabled.
|
||||
*/
|
||||
oauthDisabled?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2013,18 +2013,23 @@ describe("defineConfig", function () {
|
||||
|
||||
it("should add cloud options to the project config and relevant modules if the environment variables are set", function () {
|
||||
const originalEnv = { ...process.env }
|
||||
process.env.MEDUSA_BACKEND_URL = "test-backend-url"
|
||||
process.env.MEDUSA_CLOUD_ENVIRONMENT_HANDLE = "test-environment"
|
||||
process.env.MEDUSA_CLOUD_API_KEY = "test-api-key"
|
||||
process.env.MEDUSA_CLOUD_EMAILS_ENDPOINT = "test-emails-endpoint"
|
||||
process.env.MEDUSA_CLOUD_PAYMENTS_ENDPOINT = "test-payments-endpoint"
|
||||
process.env.MEDUSA_CLOUD_WEBHOOK_SECRET = "test-webhook-secret"
|
||||
process.env.MEDUSA_CLOUD_OAUTH_AUTHORIZE_ENDPOINT =
|
||||
"test-oauth-authorize-endpoint"
|
||||
process.env.MEDUSA_CLOUD_OAUTH_TOKEN_ENDPOINT = "test-oauth-token-endpoint"
|
||||
process.env.MEDUSA_CLOUD_OAUTH_DISABLED = "true"
|
||||
const config = defineConfig()
|
||||
process.env = { ...originalEnv }
|
||||
|
||||
expect(config).toMatchInlineSnapshot(`
|
||||
{
|
||||
"admin": {
|
||||
"backendUrl": "/",
|
||||
"backendUrl": "test-backend-url",
|
||||
"path": "/app",
|
||||
},
|
||||
"featureFlags": {},
|
||||
@@ -2035,6 +2040,15 @@ describe("defineConfig", function () {
|
||||
},
|
||||
"auth": {
|
||||
"options": {
|
||||
"cloud": {
|
||||
"api_key": "test-api-key",
|
||||
"callback_url": "test-backend-url/app/login?auth_provider=cloud",
|
||||
"disabled": true,
|
||||
"environment_handle": "test-environment",
|
||||
"oauth_authorize_endpoint": "test-oauth-authorize-endpoint",
|
||||
"oauth_token_endpoint": "test-oauth-token-endpoint",
|
||||
"sandbox_handle": undefined,
|
||||
},
|
||||
"providers": [
|
||||
{
|
||||
"id": "emailpass",
|
||||
@@ -2176,6 +2190,10 @@ describe("defineConfig", function () {
|
||||
"apiKey": "test-api-key",
|
||||
"emailsEndpoint": "test-emails-endpoint",
|
||||
"environmentHandle": "test-environment",
|
||||
"oauthAuthorizeEndpoint": "test-oauth-authorize-endpoint",
|
||||
"oauthCallbackUrl": undefined,
|
||||
"oauthDisabled": true,
|
||||
"oauthTokenEndpoint": "test-oauth-token-endpoint",
|
||||
"paymentsEndpoint": "test-payments-endpoint",
|
||||
"sandboxHandle": undefined,
|
||||
"webhookSecret": "test-webhook-secret",
|
||||
@@ -2205,20 +2223,25 @@ describe("defineConfig", function () {
|
||||
`)
|
||||
})
|
||||
|
||||
it("should add cloud options to the project config and relevant modules if the environment varianbles is set for a sandbox", function () {
|
||||
it("should add cloud options to the project config and relevant modules if the environment variable is set for a sandbox", function () {
|
||||
const originalEnv = { ...process.env }
|
||||
process.env.MEDUSA_BACKEND_URL = "test-backend-url"
|
||||
process.env.MEDUSA_CLOUD_SANDBOX_HANDLE = "test-sandbox"
|
||||
process.env.MEDUSA_CLOUD_API_KEY = "test-api-key"
|
||||
process.env.MEDUSA_CLOUD_EMAILS_ENDPOINT = "test-emails-endpoint"
|
||||
process.env.MEDUSA_CLOUD_PAYMENTS_ENDPOINT = "test-payments-endpoint"
|
||||
process.env.MEDUSA_CLOUD_WEBHOOK_SECRET = "test-webhook-secret"
|
||||
process.env.MEDUSA_CLOUD_OAUTH_AUTHORIZE_ENDPOINT =
|
||||
"test-oauth-authorize-endpoint"
|
||||
process.env.MEDUSA_CLOUD_OAUTH_TOKEN_ENDPOINT = "test-oauth-token-endpoint"
|
||||
process.env.MEDUSA_CLOUD_OAUTH_DISABLED = "true"
|
||||
const config = defineConfig()
|
||||
process.env = { ...originalEnv }
|
||||
|
||||
expect(config).toMatchInlineSnapshot(`
|
||||
{
|
||||
"admin": {
|
||||
"backendUrl": "/",
|
||||
"backendUrl": "test-backend-url",
|
||||
"path": "/app",
|
||||
},
|
||||
"featureFlags": {},
|
||||
@@ -2229,6 +2252,15 @@ describe("defineConfig", function () {
|
||||
},
|
||||
"auth": {
|
||||
"options": {
|
||||
"cloud": {
|
||||
"api_key": "test-api-key",
|
||||
"callback_url": "test-backend-url/app/login?auth_provider=cloud",
|
||||
"disabled": true,
|
||||
"environment_handle": undefined,
|
||||
"oauth_authorize_endpoint": "test-oauth-authorize-endpoint",
|
||||
"oauth_token_endpoint": "test-oauth-token-endpoint",
|
||||
"sandbox_handle": "test-sandbox",
|
||||
},
|
||||
"providers": [
|
||||
{
|
||||
"id": "emailpass",
|
||||
@@ -2370,6 +2402,10 @@ describe("defineConfig", function () {
|
||||
"apiKey": "test-api-key",
|
||||
"emailsEndpoint": "test-emails-endpoint",
|
||||
"environmentHandle": undefined,
|
||||
"oauthAuthorizeEndpoint": "test-oauth-authorize-endpoint",
|
||||
"oauthCallbackUrl": undefined,
|
||||
"oauthDisabled": true,
|
||||
"oauthTokenEndpoint": "test-oauth-token-endpoint",
|
||||
"paymentsEndpoint": "test-payments-endpoint",
|
||||
"sandboxHandle": "test-sandbox",
|
||||
"webhookSecret": "test-webhook-secret",
|
||||
@@ -2415,6 +2451,9 @@ describe("defineConfig", function () {
|
||||
webhookSecret: "overriden-webhook-secret",
|
||||
emailsEndpoint: "overriden-emails-endpoint",
|
||||
paymentsEndpoint: "overriden-payments-endpoint",
|
||||
oauthAuthorizeEndpoint: "overriden-oauth-authorize-endpoint",
|
||||
oauthTokenEndpoint: "overriden-oauth-token-endpoint",
|
||||
oauthDisabled: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -2434,6 +2473,15 @@ describe("defineConfig", function () {
|
||||
},
|
||||
"auth": {
|
||||
"options": {
|
||||
"cloud": {
|
||||
"api_key": "overriden-api-key",
|
||||
"callback_url": "//app/login?auth_provider=cloud",
|
||||
"disabled": true,
|
||||
"environment_handle": "overriden-environment",
|
||||
"oauth_authorize_endpoint": "overriden-oauth-authorize-endpoint",
|
||||
"oauth_token_endpoint": "overriden-oauth-token-endpoint",
|
||||
"sandbox_handle": undefined,
|
||||
},
|
||||
"providers": [
|
||||
{
|
||||
"id": "emailpass",
|
||||
@@ -2575,6 +2623,10 @@ describe("defineConfig", function () {
|
||||
"apiKey": "overriden-api-key",
|
||||
"emailsEndpoint": "overriden-emails-endpoint",
|
||||
"environmentHandle": "overriden-environment",
|
||||
"oauthAuthorizeEndpoint": "overriden-oauth-authorize-endpoint",
|
||||
"oauthCallbackUrl": undefined,
|
||||
"oauthDisabled": true,
|
||||
"oauthTokenEndpoint": "overriden-oauth-token-endpoint",
|
||||
"paymentsEndpoint": "overriden-payments-endpoint",
|
||||
"sandboxHandle": undefined,
|
||||
"webhookSecret": "overriden-webhook-secret",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
AdminOptions,
|
||||
ConfigModule,
|
||||
InputConfig,
|
||||
InputConfigModules,
|
||||
@@ -50,7 +51,7 @@ export function defineConfig(config: InputConfig = {}): ConfigModule {
|
||||
const projectConfig = normalizeProjectConfig(config.projectConfig, options)
|
||||
const adminConfig = normalizeAdminConfig(config.admin)
|
||||
const modules = resolveModules(config.modules, options, config.projectConfig)
|
||||
applyCloudOptionsToModules(modules, projectConfig?.cloud)
|
||||
applyCloudOptionsToModules(modules, projectConfig?.cloud, adminConfig)
|
||||
const plugins = resolvePlugins(config.plugins, options)
|
||||
|
||||
return {
|
||||
@@ -378,6 +379,11 @@ function normalizeProjectConfig(
|
||||
webhookSecret: process.env.MEDUSA_CLOUD_WEBHOOK_SECRET,
|
||||
emailsEndpoint: process.env.MEDUSA_CLOUD_EMAILS_ENDPOINT,
|
||||
paymentsEndpoint: process.env.MEDUSA_CLOUD_PAYMENTS_ENDPOINT,
|
||||
oauthAuthorizeEndpoint: process.env.MEDUSA_CLOUD_OAUTH_AUTHORIZE_ENDPOINT,
|
||||
oauthTokenEndpoint: process.env.MEDUSA_CLOUD_OAUTH_TOKEN_ENDPOINT,
|
||||
oauthCallbackUrl: process.env.MEDUSA_CLOUD_OAUTH_CALLBACK_URL,
|
||||
oauthDisabled:
|
||||
process.env.MEDUSA_CLOUD_OAUTH_DISABLED === "true" ? true : undefined,
|
||||
...cloud,
|
||||
}
|
||||
const hasCloudOptions = Object.values(mergedCloudOptions).some(
|
||||
@@ -449,6 +455,21 @@ function normalizeProjectConfig(
|
||||
...restOfProjectConfig,
|
||||
} satisfies ConfigModule["projectConfig"]
|
||||
|
||||
if (
|
||||
isCloud &&
|
||||
!mergedCloudOptions.oauthDisabled &&
|
||||
mergedCloudOptions.oauthAuthorizeEndpoint &&
|
||||
mergedCloudOptions.oauthTokenEndpoint
|
||||
) {
|
||||
const userAuthMethods = config.http.authMethodsPerActor?.user ?? [
|
||||
"emailpass",
|
||||
]
|
||||
config.http.authMethodsPerActor = {
|
||||
...config.http.authMethodsPerActor,
|
||||
user: userAuthMethods.concat("cloud"),
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -468,7 +489,8 @@ function normalizeAdminConfig(
|
||||
|
||||
function applyCloudOptionsToModules(
|
||||
modules: Exclude<ConfigModule["modules"], undefined>,
|
||||
config?: MedusaCloudOptions
|
||||
config?: MedusaCloudOptions,
|
||||
adminConfig?: AdminOptions
|
||||
) {
|
||||
if (!config) {
|
||||
return
|
||||
@@ -504,6 +526,24 @@ function applyCloudOptionsToModules(
|
||||
...(module.options ?? {}),
|
||||
}
|
||||
break
|
||||
case Modules.AUTH:
|
||||
let callbackUrl = config.oauthCallbackUrl
|
||||
if (!callbackUrl && adminConfig?.backendUrl) {
|
||||
callbackUrl = `${adminConfig?.backendUrl}${adminConfig?.path}/login?auth_provider=cloud`
|
||||
}
|
||||
module.options = {
|
||||
cloud: {
|
||||
oauth_authorize_endpoint: config.oauthAuthorizeEndpoint,
|
||||
oauth_token_endpoint: config.oauthTokenEndpoint,
|
||||
environment_handle: config.environmentHandle,
|
||||
sandbox_handle: config.sandboxHandle,
|
||||
api_key: config.apiKey,
|
||||
callback_url: callbackUrl,
|
||||
disabled: config.oauthDisabled,
|
||||
},
|
||||
...(module.options ?? {}),
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user