Link an Additional Auth Provider to an Existing Actor

In this guide, you'll learn how to allow an existing actor (such as a customer) to authenticate with an additional authentication provider. This is useful when you want to let a customer who registered with the Emailpass provider also log in with Google or a Magic Link, for example.

Why an Additional Provider Doesn't Resolve to the Actor#

Each authentication provider creates its own auth identity for an actor. The auth identity holds an app_metadata property that associates it with an actor, such as a customer, using the key {actor_type}_id:

Code
1{2  "app_metadata": {3    "customer_id": "cus_123"4  }5}

When an actor registers with the Emailpass provider, its auth identity has the {actor_type}_id key set to the associated actor's ID. So, authenticating with Emailpass resolves to that actor.

However, when the same actor authenticates with a different provider for the first time, that provider creates a separate auth identity. This new auth identity has an empty app_metadata, so it isn't associated with any actor yet:

Code
1{2  "app_metadata": {}3}

Since the new auth identity isn't associated with an actor, Medusa treats the authentication as if a new actor is registering, rather than an existing actor logging in. So, authenticating with the new provider returns a token that has an auth_identity_id but no actor_id. As a result, requests to protected routes, such as GET /store/customers/me, are rejected with a 401 error.

To allow the new provider to resolve to the existing customer, you must set the {actor_type}_id key of the new auth identity's app_metadata to the existing actor's ID. You do that with the setAuthAppMetadataStep.

Note: The rest of this guide uses the customer actor type as an example, but the same approach applies to admin users and custom actor types. Replace customer with the relevant actor type.

Step 1: Create the Linking Workflow#

Create a workflow that finds the existing customer matching the new auth identity and associates the auth identity with that customer.

Create the file src/workflows/link-customer-identity.ts with the following content:

src/workflows/link-customer-identity.ts
1import {2  createWorkflow,3  createStep,4  StepResponse,5  transform,6  WorkflowResponse,7} from "@medusajs/framework/workflows-sdk"8import {9  setAuthAppMetadataStep,10  useQueryGraphStep,11} from "@medusajs/medusa/core-flows"12import { MedusaError } from "@medusajs/framework/utils"13import { CustomerDTO } from "@medusajs/framework/types"14
15type WorkflowInput = {16  auth_identity_id: string17}18
19type AuthIdentityData = {20  provider_identities?: { entity_id: string }[]21}[]22
23const getIdentityEmailStep = createStep(24  "get-identity-email",25  ({ authIdentities }: { authIdentities: AuthIdentityData }) => {26    const email =27      authIdentities[0]?.provider_identities?.[0]?.entity_id28
29    if (!email) {30      throw new MedusaError(31        MedusaError.Types.INVALID_DATA,32        "Couldn't determine the identity's email."33      )34    }35
36    return new StepResponse(email)37  }38)39
40const validateCustomerExistsStep = createStep(41  "validate-customer-exists",42  ({43    customers,44    email,45  }: {46    customers: CustomerDTO[]47    email: string48  }) => {49    if (!customers.length) {50      throw new MedusaError(51        MedusaError.Types.NOT_FOUND,52        `No customer found with email ${email}`53      )54    }55
56    return new StepResponse(customers[0])57  }58)59
60export const linkCustomerIdentityWorkflow = createWorkflow(61  "link-customer-identity",62  (input: WorkflowInput) => {63    const { data: authIdentities } = useQueryGraphStep({64      entity: "auth_identity",65      fields: ["provider_identities.entity_id"],66      filters: {67        id: input.auth_identity_id,68      },69    })70
71    const email = getIdentityEmailStep({ authIdentities })72
73    const { data: customers } = useQueryGraphStep({74      entity: "customer",75      fields: ["id"],76      filters: {77        email,78      },79    }).config({ name: "get-customer" })80
81    const customer = validateCustomerExistsStep({82      customers,83      email,84    })85
86    const stepInput = transform(87      { input, customer },88      ({ input, customer }) => ({89        authIdentityId: input.auth_identity_id,90        actorType: "customer",91        value: customer.id,92      })93    )94
95    setAuthAppMetadataStep(stepInput)96
97    return new WorkflowResponse({98      customer_id: customer.id,99    })100  }101)

The workflow accepts the ID of the auth identity created by the new provider. Then, it:

  1. Retrieves the auth identity with its provider identity using useQueryGraphStep. The provider identity's entity_id holds the identifier the provider authenticated the customer with, such as the email.
  2. Extracts and validates the email using the getIdentityEmailStep, throwing an error if the identity has no email.
  3. Finds the existing customer by that email using useQueryGraphStep again. Adjust the filters if your provider identifies customers by a property other than the email.
  4. Validates that a customer was found using the validateCustomerExistsStep, throwing an error if not.
  5. Prepares the step's input using transform, then associates the new auth identity with the existing customer using the setAuthAppMetadataStep.
Note: Since you call useQueryGraphStep twice in the same workflow, you must set a unique name for the second call with .config({ name: "get-customer" }). Otherwise, the workflow throws an error for having two steps with the same ID.
Warning: Derive the email from the authenticated auth identity, as shown above, rather than accepting it in the request body. Otherwise, a customer could link their auth identity to another customer's account by passing a different email.
Tip: The setAuthAppMetadataStep throws an error if the auth identity is already associated with the same actor type. So, this workflow only succeeds for a new auth identity that isn't linked to a customer yet.

Step 2: Create the Linking API Route#

Next, create an API route that executes the workflow after the customer authenticates with the new provider.

Create the file src/api/auth/link/route.ts with the following content:

src/api/auth/link/route.ts
1import {2  AuthenticatedMedusaRequest,3  MedusaResponse,4} from "@medusajs/framework/http"5import { MedusaError } from "@medusajs/framework/utils"6import { linkCustomerIdentityWorkflow } from "../../../workflows/link-customer-identity"7
8export const POST = async (9  req: AuthenticatedMedusaRequest,10  res: MedusaResponse11) => {12  // The auth identity is already linked to a customer.13  if (req.auth_context.actor_id) {14    throw new MedusaError(15      MedusaError.Types.INVALID_DATA,16      "Auth identity is already linked to a customer."17    )18  }19
20  const { result } = await linkCustomerIdentityWorkflow(req.scope)21    .run({22      input: {23        auth_identity_id: req.auth_context.auth_identity_id,24      },25    })26
27  res.status(200).json(result)28}

In the API route, you:

  1. Reject the request if the auth identity is already linked to a customer, indicated by the actor_id property in the request's auth_context.
  2. Execute the linkCustomerIdentityWorkflow, passing it the authenticated auth identity's ID from the request's auth_context.

Refer to the Auth Flow with Routes guide to learn how the auth_context is set on the request.


Step 3: Protect the API Route#

The customer hasn't been linked to an actor yet when they access the linking route, so you must allow unregistered auth identities to access it.

To do that, apply the authenticate middleware to the route with the allowUnregistered option enabled.

Create the file src/api/middlewares.ts with the following content, or add the route to your existing middlewares:

src/api/middlewares.ts
1import {2  defineMiddlewares,3  authenticate,4} from "@medusajs/framework/http"5
6export default defineMiddlewares({7  routes: [8    {9      matcher: "/auth/link",10      method: "POST",11      middlewares: [12        authenticate("customer", ["bearer"], {13          allowUnregistered: true,14        }),15      ],16    },17  ],18})

This protects the /auth/link route so that only auth identities of the customer actor type, including unregistered ones, can access it.


Test the Linking Flow#

To link the new provider to an existing customer:

  1. Authenticate the customer with the new provider to obtain a token. Refer to the Auth Flow with Routes guide for the authentication steps.
  2. Send a POST request to /auth/link with the token in the Authorization header:
Code
1curl -X POST http://localhost:9000/auth/link \2  -H "Authorization: Bearer {token}"

Afterwards, the customer can authenticate with the new provider and their token will resolve to the existing customer.

Warning: Only link providers that verify the customer owns the identity, such as an email the provider confirmed. Otherwise, a customer could link their auth identity to another customer's account and gain access to it.

To remove the association between an auth identity and a customer, such as when a customer disconnects a provider, pass null as the value to the setAuthAppMetadataStep:

Code
1setAuthAppMetadataStep({2  authIdentityId: "au_123",3  actorType: "customer",4  value: null,5})

Learn more about the setAuthAppMetadataStep in the setAuthAppMetadataWorkflow reference.

Was this page helpful?
Ask Bloom
For assistance in your development, use Claude Code Plugins or Docs MCP server in Cursor, VSCode, etc...FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break
⇧↵