www/apps/resources/app/storefront-development/customers/third-party-login/page.mdx
import { Prerequisites, CodeTabs, CodeTab, Details } from "docs-ui"
export const metadata = {
title: Third-Party or Social Login in Storefront,
}
In this guide, you'll learn how to implement third-party or social login in your storefront. You'll implement the flow using Google as an example.
By following the steps in this guide, you'll learn how to:
These are the pages you need in your storefront to allow customers to log in or create an account using a third-party service.
<Prerequisites items={[ { text: "Google OAuth credentials configured. This is required for using the Google Auth Module Provider.", }, { text: "Install the Google Auth Module Provider in your Medusa application, or the provider you're using.", link: "/commerce-modules/auth/auth-providers/google" }, { text: "JS SDK installed and configured in your storefront, with the authentication method you're using (JWT or session) configured.", link: "../login/page.mdx#login-customer-methods" } ]} />
In your storefront, you'll have a login page with different login options. One of those options will be a button that starts the third-party login process. For example, a "Login with Google" button.
When the customer clicks the "Login with Google" button, send a request to the Authenticate Customer API route. This returns the URL to redirect the customer to Google for authentication.
For example:
<Note title="Tip">Learn how to install and configure the JS SDK in the JS SDK documentation.
</Note> <CodeTabs group="authenticated-request"> <CodeTab label="React" value="react">export const reactHighlights = [ ["7", "login", "Send a request to the Authenticate Customer API route"], ["9", "result.location", "If the request returns a location, redirect to that location to continue the authentication"], ["16", "", "If the token isn't returned, the authentication has failed"], ["24", "retrieve", "Retrieve the customer's details as an example of testing authentication"] ]
"use client" // include with Next.js 13+
import { sdk } from "@/lib/sdk"
export default function Login() {
const loginWithGoogle = async () => {
const result = await sdk.auth.login("customer", "google", {})
if (typeof result === "object" && result.location) {
// redirect to Google for authentication
window.location.href = result.location
return
}
if (typeof result !== "string") {
// result failed, show an error
alert("Authentication failed")
return
}
// Customer was previously authenticated, and its token is now stored in the JS SDK.
// all subsequent requests are authenticated
const { customer } = await sdk.store.customer.retrieve()
console.log(customer)
}
return (
<div>
<button onClick={loginWithGoogle}>Login with Google</button>
</div>
)
}
export const jsSdkHighlights = [ ["2", "login", "Send a request to the Authenticate Customer API route"], ["4", "", "If the request returns a location, redirect to that location to continue the authentication"], ["11", "", "If the token isn't returned, the authentication has failed"], ["19", "retrieve", "Retrieve the customer's details as an example of testing authentication"] ]
const loginWithGoogle = async () => {
const result = await sdk.auth.login("customer", "google", {})
if (typeof result === "object" && result.location) {
// redirect to Google for authentication
window.location.href = result.location
return
}
if (typeof result !== "string") {
// result failed, show an error
alert("Authentication failed")
return
}
// Customer was previously authenticated, and its token is now stored in the JS SDK.
// all subsequent requests are authenticated
const { customer } = await sdk.store.customer.retrieve()
console.log(customer)
}
You define a loginWithGoogle function that:
/auth/customer/google API route using the JS SDK's auth.login method.
google in the login method with your provider ID.location property, redirect to the returned page for authentication with the third-party service.store.customer.retrieve method.The JS SDK sets and passes authentication headers or session cookies automatically based on your configured authentication method. If you're not using the JS SDK, make sure to pass the necessary headers in your request as explained in the API reference.
</Note>After the customer clicks the "Login with Google" button, they're redirected to Google to authenticate. Once they authenticate with Google, they're redirected back to your storefront to the callback page. You set this page's URL in Google's OAuth credentials configurations.
In this step, you'll create the callback page that handles the response from Google and creates or retrieves the customer account. You'll implement the page step-by-step to explain the different parts of the flow. You can copy the full page code from the Full Code Example for Third-Party Login Callback Page section.
First, install the react-jwt library in your storefront:
npm install react-jwt
You'll use it to decode the token that Medusa returns after validating the authentication callback.
Then, create a new page in your storefront that will be used as the callback/redirect URI destination:
<CodeTabs group="authenticated-request"> <CodeTab label="React" value="react">export const sendCallbackReactHighlights = [
["12", "queryParams", "The query parameters received from Google, such as code and state"],
["21", "callback", "Send a request to the Validate Authentication Callback API route"],
["28", "catch", "If an error occurs, show an alert and exit execution"]
]
"use client" // include with Next.js 13+
import { HttpTypes } from "@medusajs/types"
import { useEffect, useMemo, useState } from "react"
import { decodeToken } from "react-jwt"
import { sdk } from "@/lib/sdk"
export default function GoogleCallback() {
const [loading, setLoading] = useState(true)
const [customer, setCustomer] = useState<HttpTypes.StoreCustomer>()
// for other than Next.js
const queryParams = useMemo(() => {
const searchParams = new URLSearchParams(window.location.search)
return Object.fromEntries(searchParams.entries())
}, [])
const sendCallback = async () => {
let token = ""
try {
token = await sdk.auth.callback(
"customer",
"google",
// pass all query parameters received from the
// third party provider
queryParams
)
} catch (error) {
alert("Authentication Failed")
throw error
}
return token
}
// TODO add more functions
return (
<div>
{loading && <span>Loading...</span>}
{customer && <span>Created customer {customer.email} with Google.</span>}
</div>
)
}
export const sendCallbackFetchHighlights = [
["6", "queryParams", "The query parameters received from Google, such as code and state"],
["12", "callback", "Send a request to the Validate Authentication Callback API route"],
["19", "catch", "If an error occurs, show an alert and exit execution"]
]
import { decodeToken } from "react-jwt"
// ...
const searchParams = new URLSearchParams(window.location.search)
const queryParams = Object.fromEntries(searchParams.entries())
const sendCallback = async () => {
let token = ""
try {
token = await sdk.auth.callback(
"customer",
"google",
// pass all query parameters received from the
// third party provider
queryParams
)
} catch (error) {
alert("Authentication Failed")
throw error
}
return token
}
// TODO add more functions...
You add a new page. In the page's component, you define the sendCallback function that sends a request to the Validate Callback API route, passing it all query parameters received from Google. These include the code and state parameters.
The JS SDK stores the JWT token returned by the Validate Callback API route automatically. It attaches this token to subsequent requests. If you're building this authentication flow without the JS SDK, you need to pass it manually to the next requests.
</Note>Next, you'll add to the page a function that creates a customer. You'll use this function if the customer is authenticating with the third-party service for the first time.
Replace the TODO after the sendCallback function with the following:
export const createCustomerHighlights = [ ["3", "create", "Create a customer"] ]
const createCustomer = async (email: string) => {
// create customer
await sdk.store.customer.create({
email,
})
}
// TODO add more functions...
You add the function createCustomer which creates a customer when this is the first time the customer is authenticating with the third-party service.
This method assumes that the token received from the Validate Callback API route is already set in the JS SDK. So, if you're implementing this flow without using the JS SDK, make sure to pass the token received from the Validate Callback API route in the authorization Bearer header.
</Note>Next, you'll add to the page a function that refreshes the authentication token after creating the customer. This is necessary to ensure that the token includes the created customer's details.
Replace the new TODO with the following:
export const refreshTokenHighlights = [ ["3", "refresh", "Fetch a new token for the created customer"] ]
const refreshToken = async () => {
// refresh the token
const result = await sdk.auth.refresh()
}
// TODO add more functions...
You add the function refreshToken that sends a request to the Refresh Token API route to retrieve a new token for the created customer.
The refreshToken method also updates the token stored by the JS SDK, ensuring that subsequent requests use that token.
This method assumes that the token received from the Validate Callback API route is already set in the JS SDK. So, if you're implementing this flow without using the JS SDK, make sure to pass the token in the authorization Bearer header. Make sure to also update the token stored in your application after refreshing it.
</Note>Finally, you'll add to the page a function that validates the authentication callback in Medusa and creates or retrieves the customer account. It will use the functions added earlier.
Add in the place of the new TODO the validateCallback function that runs when the page first loads to validate the authentication:
export const validateReactHighlights = [
["2", "sendCallback", "Validate the callback in Medusa and retrieve the authentication token"],
["6", "shouldCreateCustomer", "Check if the decoded token has an actor_id property to decide whether a customer needs to be created"],
["9", "createCustomer", "Create a customer if the decoded token doesn't have actor_id"],
["11", "refreshToken", "Fetch a new token for the created customer"],
["15", "retrieve", "Retrieve the customer's details as an example of testing authentication"]
]
const validateCallback = async () => {
const token = await sendCallback()
const decodedToken = decodeToken(token) as { actor_id: string, user_metadata: Record<string, unknown> }
const shouldCreateCustomer = decodedToken.actor_id === ""
if (shouldCreateCustomer) {
await createCustomer(decodedToken.user_metadata.email as string)
await refreshToken()
}
// use token to send authenticated requests
const { customer: customerData } = await sdk.store.customer.retrieve()
setCustomer(customerData)
setLoading(false)
}
// TODO run validateCallback when the page loads
The validateCallback function uses the functions added earlier to implement the following flow:
sendCallback function also sets the token in the JS SDK. This passes the token in subsequent requests.actor_id property.user_metadata in the decoded token will hold the customer's information in the third-party provider, such as their email. You can use this value as the customer's email in Medusa.Finally, you need to run the validateCallback function when the page first loads. In React, you can do that using the useEffect hook.
For example, replace the last TODO with the following:
useEffect(() => {
if (!loading) {
return
}
validateCallback()
}, [loading])
This runs the validateCallback function when the page first loads. If the validation is successful, the customer is authenticated.
You can show a success message or redirect the customer to another page. For example, use another useEffect hook to redirect the customer to the homepage after successful authentication:
useEffect(() => {
if (!customer) {
return
}
// redirect to homepage after successful authentication
window.location.href = "/"
}, [customer])
"use client" // include with Next.js 13+
import { HttpTypes } from "@medusajs/types"
import { useEffect, useMemo, useState } from "react"
import { decodeToken } from "react-jwt"
import { sdk } from "@/lib/sdk"
export default function GoogleCallback() {
const [loading, setLoading] = useState(true)
const [customer, setCustomer] = useState<HttpTypes.StoreCustomer>()
// for other than Next.js
const queryParams = useMemo(() => {
const searchParams = new URLSearchParams(window.location.search)
return Object.fromEntries(searchParams.entries())
}, [])
const sendCallback = async () => {
let token = ""
try {
token = await sdk.auth.callback(
"customer",
"google",
// pass all query parameters received from the
// third party provider
queryParams
)
} catch (error) {
alert("Authentication Failed")
throw error
}
return token
}
const createCustomer = async () => {
// create customer
await sdk.store.customer.create({
email: "[email protected]",
})
}
const refreshToken = async () => {
// refresh the token
const result = await sdk.auth.refresh()
}
const validateCallback = async () => {
const token = await sendCallback()
const decodedToken = decodeToken(token) as { actor_id: string, user_metadata: Record<string, unknown> }
const shouldCreateCustomer = decodedToken.actor_id === ""
if (shouldCreateCustomer) {
await createCustomer(decodedToken.user_metadata.email as string)
await refreshToken()
}
// use token to send authenticated requests
const { customer: customerData } = await sdk.store.customer.retrieve()
setCustomer(customerData)
setLoading(false)
}
useEffect(() => {
if (!loading) {
return
}
validateCallback()
}, [loading])
useEffect(() => {
if (!customer) {
return
}
// TODO redirect to homepage after successful authentication
}, [customer])
return (
<div>
{loading && <span>Loading...</span>}
{customer && <span>Created customer {customer.email} with Google.</span>}
</div>
)
}
import { decodeToken } from "react-jwt"
// ...
const queryParams = new URLSearchParams(window.location.search)
const code = queryParams.get("code")
const state = queryParams.get("state")
const sendCallback = async () => {
let token = ""
try {
token = await sdk.auth.callback(
"customer",
"google",
// pass all query parameters received from the
// third party provider
queryParams
)
} catch (error) {
alert("Authentication Failed")
throw error
}
return token
}
const createCustomer = async () => {
// create customer
await sdk.store.customer.create({
email: "[email protected]",
})
}
const refreshToken = async () => {
// refresh the token
const result = await sdk.auth.refresh()
}
const validateCallback = async () => {
const token = await sendCallback()
const shouldCreateCustomer = (decodeToken(token) as { actor_id: string }).actor_id === ""
if (shouldCreateCustomer) {
await createCustomer()
await refreshToken()
}
// all subsequent requests are authenticated
const { customer: customerData } = await sdk.store.customer.retrieve()
setCustomer(customerData)
setLoading(false)
}
In this section, you'll find a general overview of the third-party login flow in the storefront. This is useful if you're not using the JS SDK or want to understand the flow better.
If you already set up the Auth Module Provider in your Medusa application, you can log in a customer with a third-party service, such as Google or GitHub, using the following flow:
location property to authenticate with a third-party service, such as Google. When you receive this property, redirect to the returned location.token property. In this case, the customer was previously logged in with the third-party service. No additional actions are required. You can use the token to send subsequent authenticated requests.code and state. Make sure your third-party service is configured to redirect to your storefront's callback page after successful authentication.code, state, etc.) received from the third-party service.
actor_id property, the customer is already registered. Use this token for subsequent authenticated requests.