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:
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:
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.
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:
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:
- Retrieves the auth identity with its provider identity using useQueryGraphStep. The provider identity's
entity_idholds the identifier the provider authenticated the customer with, such as the email. - Extracts and validates the email using the
getIdentityEmailStep, throwing an error if the identity has no email. - Finds the existing customer by that email using
useQueryGraphStepagain. Adjust thefiltersif your provider identifies customers by a property other than the email. - Validates that a customer was found using the
validateCustomerExistsStep, throwing an error if not. - Prepares the step's input using
transform, then associates the new auth identity with the existing customer using thesetAuthAppMetadataStep.
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.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.ts1import {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.) {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 : req.auth_context.auth_identity_id,24 },25 })26 27 res.status(200).json(result)28}
In the API route, you:
- Reject the request if the auth identity is already linked to a customer, indicated by the
actor_idproperty in the request'sauth_context. - Execute the
linkCustomerIdentityWorkflow, passing it the authenticated auth identity's ID from the request'sauth_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:
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:
- Authenticate the customer with the new provider to obtain a token. Refer to the Auth Flow with Routes guide for the authentication steps.
- Send a
POSTrequest to/auth/linkwith the token in theAuthorizationheader:
Afterwards, the customer can authenticate with the new provider and their token will resolve to the existing customer.
Unlink a Provider#
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:
Learn more about the setAuthAppMetadataStep in the setAuthAppMetadataWorkflow reference.