Table of Contents
Prerequisites
This tutorial was written by Matt Derman, mathematical statistics and computer science grad who decided they much prefer building things to theory. Connect with them on X and LinkedIn to see more of their work!
Single sign-on (SSO) allows users to sign in to multiple services with a single login. If you use Gmail, you may have noticed that you don't need to sign in to Google Drive or Google Photos. That's SSO making your life easier!
Imagine that you're part of a business that has more than twenty internal apps (used by your staff and clients who all have Microsoft Azure accounts). These apps might contain confidential information or financial data, so you want to make sure that not everyone can access every app and have the same permissions.
But you also don't want to have to build separate authentication systems for each app, manage users, and send password reset emails, among other tasks. With SSO and a tool like Descope, all your users can sign in once and be logged in to all your apps everywhere.
In this article, you will secure a Remix app using OpenID Connect (OIDC) SSO, Security Assertion Markup Language (SAML) SSO, and magic link authentication with Descope. The app you'll be building is an AI chat application that generates images using OpenAI's image API and has light/dark mode using Shadcn.
Prerequisites
To complete the tutorial, you'll need the following:
Access to an Azure account
npm and Git installed
An OpenAI API key, which can be obtained from the OpenAI developer dashboard
You can find the full code in this GitHub repository.
Creating a Descope project and customizing the flow
Log in on the Descope website, or sign up if you don't have an account.
If you have just created a new account, you will be redirected to the Getting Started page for your new default project.
If you want to create a new project, click on your project name in the top left (next to the Descope icon) and choose + Project from the dropdown. Creating a new project will take you to the Getting Started page.
Now that you are on the Who uses your application? page of Getting Started, choose the Business option since this will allow us to specify tenants for the SSO implementation later. Tenants represent entities such as your employees or your client's users. Next, choose Magic Link and then SSO.

On the page after that, choose Go ahead without MFA as you don't need this yet. Then, click Next. Now, your project setup is complete. You can edit the name of your project by going to Settings > Project in the sidebar on the left.
You will only be implementing and using the flow Sign up or in, so if you ever want to customize this flow, go to Build > Flows > Sign up or in.
The flow template sign-up-or-sign-in.json
is provided in the root of the GitHub repository. To follow this tutorial, you will need to import this flow by going to Build > Flows, clicking on Import flow, and then selecting the JSON file.

Creating the Remix app with authentication
In the provided repository, there are two branches: starter and master. Master is the completed project. In order to follow this tutorial, you must check out the starter branch.
Once you are in the starter branch, go to app/routes
and have a look at the _index.tsx
file (which relates to your base route at http://locahost:3000). You will see that a Remix loader is declared, which asynchronously loads an example image. The component that consumes previousChat
is wrapped in a React <Suspense>. Just like that, you have data that displays in the app once it's ready!
export async function loader() {
return defer({
previousChat: loadPreviousChat(),
});
}
In the same file, you declare the React component that will use the data (via the useLoaderData
hook) that is asynchronously returned by the loadPreviousChat()
function above:
const { previousChat } = useLoaderData<typeof loader>();
const actionData = useActionData<ActionData>();
const hasImageResult =
actionData && "imageUrl" in actionData && actionData.imageUrl;
return (
<SidebarProvider>
{isClient && <AppSidebar />}
<SidebarInset className="flex flex-col">
<Header />
<div className="flex flex-1 flex-col items-center justify-end p-4 pt-0">
<div className="w-full max-w-3xl mx-auto">
<PreviousExampleChatSession previousChatPromise={previousChat} />
{hasImageResult &&
You'll also notice in the code above that the app-sidebar.client.tsx (AppSidebar)
component in app/components/nav
is only rendered if isClient
is true. This browser-only rendering logic is important, and you will use it later to render auth-wrapper.client.tsx
in app/components/auth
.
In the same _index.tsx
file in app/routes
, there is a Remix action that will hit the OpenAI API and generate an image with the user's prompt:
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const prompt = formData.get("prompt") as string;
if (!prompt) {
return json<ActionData>({ error: "Prompt is required" });
}
This action is triggered by a button within the chat-command-menu.tsx
file, specifically from inside a Form
element. By default in Remix, a button with type="submit"
inside a form will send an HTTP POST request to the action defined for that route. If you specify the action prop on the form, it will use a different action. See the file resources.theme-toggle.tsx
in app/routes
for an example of this.
This makes submitting data and forms really simple, but unfortunately, it isn't typed. As you can see in the above code, to access the prompt that the user submitted, you have to access the value by typing in the key name as a string and casting the result to the string:
const prompt = formData.get("prompt") as string;
However, considering how simple useLoaderData()
makes managing state with Remix, this isn't a huge issue. What isn't so simple is securing this application so real people can use it with their own private AI chats.
Setting up Descope authentication in Remix
You can now use the sign-up-or-sign-in.json
flow that you imported into Descope earlier in your Remix application.
First, go to https://app.descope.com/settings/project and copy the project ID. Now, create a file called .env
in the root of the repository and add PUBLIC_DESCOPE_PROJECT_ID={your_project_id}
. You also need to add OPENAI_API_KEY={you_openai_key}
for the image generation to work.
Then, install the Descope React SDK with the command below:
npm i --save @descope/react-sdk
You also need to create the AuthWrapper
component, which will display three different views based on the user's authentication state—logged in, not logged in, or loading—while Descope checks their session. Create a file in app/components/auth
called auth-wrapper.client.tsx
and paste in the code below:
import { AuthProvider, useSession, useUser, Descope } from "@descope/react-sdk";
import { Outlet } from "@remix-run/react";
import { toast } from "~/hooks/use-toast";
export function AuthWrapper({ children }: { children: React.ReactNode }) {
return (
<AuthProvider projectId={window.ENV.PUBLIC_DESCOPE_PROJECT_ID ?? ""}>
<AuthInner>
<Outlet />
</AuthInner>
</AuthProvider>
);
}
function AuthInner({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isSessionLoading } = useSession();
const { isUserLoading } = useUser();
return (
<>
{!isAuthenticated && (
<div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-900 transition-colors">
<div className="w-full max-w-md p-6 bg-white dark:bg-gray-800 rounded-lg shadow-md dark:shadow-gray-700">
<Descope
flowId="sign-up-or-in"
onSuccess={(e) => console.log(e.detail.user)}
onError={(e) =>
toast({
title: "Could not log in!",
description: "Please try again.",
variant: "destructive",
})
}
/>
</div>
</div>
)}
{isAuthenticated && (isSessionLoading || isUserLoading) && (
<div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-900 transition-colors">
<div className="w-full max-w-md p-6 bg-white dark:bg-gray-800 rounded-lg shadow-md dark:shadow-gray-700 text-center">
<p className="text-gray-700 dark:text-gray-300">Loading...</p>
</div>
</div>
)}
{isAuthenticated && !isSessionLoading && !isUserLoading && <Outlet />}
</>
);
}
Now, you will need to change the app/root.tsx
file. First, add these imports to the top of the file:
import { AuthWrapper } from "./components/auth/auth-wrapper.client";
import { isClient } from "./lib/utils";
Change the environment variable in the loader (PUBLIC_DESCOPE_PROJECT_ID
) so it will load your Descope project ID in into the client:
export const loader = async ({ request }: LoaderFunctionArgs) => {
return json({
requestInfo: {
hints: getHints(request),
userPrefs: {
theme: getTheme(request),
},
},
ENV: {
PUBLIC_DESCOPE_PROJECT_ID: process.env.PUBLIC_DESCOPE_PROJECT_ID,
},
});
};
The last thing to do in this file is scroll down to where you see <Outlet/>
and replace it with the following:
{isClient && (
<AuthWrapper>
<Outlet />
</AuthWrapper>
)}
Now, you will need to update the app-sidebar.client.tsx
file in app/components/nav
so that this client-side navbar will load the user information from Descope once you have logged in.
Add this import to the file:
import { useUser } from "@descope/react-sdk";
Go to this user declaration starting on line 44:
const user = {
userId: "123",
email: "joe@example.com",
name: "Example User",
picture: "NA",
};
Replace the above with the user that is obtained from the Descope useUser()
hook:
const { user } = useUser();
The last thing to do is implement the logout functionality. Go to the nav-user.client.tsx
in app/components/nav/
and add this import:
import { useDescope } from "@descope/react-sdk"
At the top of the NavUser
function, add this code:
const { logout } = useDescope()
const handleLogout = useCallback(() => {
logout()
}, [logout])
The DropdownMenuItem
near the bottom of the file currently has a placeholder onClick
event handler:
<DropdownMenuItem onClick={() => {}} className="flex items-center gap-2 text-destructive focus:text-destructive">
<LogOut className="size-4" />
<span>Log out</span>
</DropdownMenuItem>
Change this so that you can use the new handleLogout
function:
<DropdownMenuItem onClick={handleLogout} className="flex items-center gap-2 text-destructive focus:text-destructive">
<LogOut className="size-4" />
<span>Log out</span>
</DropdownMenuItem>
That's it! If you start up the app now, it will display the Descope welcome screen.

You can now sign in with a magic link, and the home screen will display your user information in the bottom left.
Lastly, if you want to access the token that Descope has provided your browser (after authenticating), you can access it like so. This example is from https://docs.descope.com/getting-started/react/nodejs:
import { getSessionToken } from '@descope/react-sdk';
const App = () => {
const { isAuthenticated, isSessionLoading } = useSession()
const { user, isUserLoading } = useUser()
const exampleFetchCall = async () => {
const sessionToken = getSessionToken();
// Example Fetch Call with HTTP Authentication Header
fetch('your_application_server_url', {
headers: {
Accept: 'application/json',
Authorization: 'Bearer ' + sessionToken,
}
})
}
Then, you can pass it in headers to your backend or whatever else you may need to use it for. Thereafter, it can be verified using the Descope package for your backend. For instance, with Node.js, install as follows:
npm i --save @descope/node-sdk
This is out of the scope of this tutorial, but you can read more at https://docs.descope.com/getting-started/react/nodejs.
Adding OIDC SSO with Descope and Azure
If you log out now and try to log in using the Continue with SSO button, you will notice it fails. This is because you need to set up a tenant for SSO in Descope and then configure the application in Azure.
You're using Azure for this tutorial, but remember that this could be done in Okta, Google Cloud, and more.
The first step is to create an application within Azure that is associated with your Descope project, so navigate to https://portal.azure.com/#view/Microsoft_AAD_IAM/AppGalleryBladeV2 to create the Azure application. Click Create your own application in the top left of the page (underneath the heading Browse Microsoft Entra Gallery). Enter the name of the application, and check the Register an application to integrate with Microsoft Entra ID (App you're developing) option under What are you looking to do with your application?

On the following page, you need to set the redirect uri as https://api.descope.com/v1/oauth/callback
.
That's all you need to change within Azure for this new application. The next step is to add application details in Descope. First, go to the app registration page and click on the app you just created.
Then, go to the Overview tab, copy the Application (client) ID, and store it somewhere for you to retrieve shortly. You need to obtain a secret for this application, so go to Manage > Certificates & Secrets and ensure you are on the Client secrets (0) tab. Create a new client secret by clicking the + New client secret button, and then copy the value.
Now that you've stored the client ID and secret, go to the Authentication tab directly above Certificates & Secrets, and check the two checkboxes, Access and ID tokens, respectively. Lastly, you need to paste https://api.descope.com/oauth2/v1/logout
into Front-channel logout URL.

You also have just set up the app in Azure for your first tenant, but you don't have the tenant modeled in Descope, so let's create it.
Go to https://app.descope.com/tenants and click the plus symbol to create a new tenant. Then, navigate to the SSO page, as indicated below. Select OIDC and use the same tenant in Descope but different tenants in Azure.

Now, scroll down to SSO Configuration and enter this information under Account Settings:
Provider Name: Azure
Client ID: The application (client) ID you saved earlier
Client Secret: The secret you saved earlier
Scope:
openid profile email
Grant Type:
Authorization code
(as is)
To fill in the Connections Settings section underneath, you will need to head back to your app in Azure. Go to the Overview page again, and click on the Endpoints tab at the top. Fill in the following two inputs under Connections Settings in Descope using the value from the corresponding section in Azure.
Authorization Endpoint: The value under OAuth 2.0 authorization endpoint (v2)
Token Endpoint: The value under OAuth 2.0 token endpoint (v2)
Then, open the link under OpenID Connect metadata document in a new tab, and check pretty print so you can see the fields more easily.
Now, you can fill in the last fields in Descope using the information on this page.
Issuer: The value to the right of
issuer
in the JSON fileUser Info Endpoint: The value for
userinfo_endpoint
To test whether OIDC has been configured correctly, head back to the Remix app and make sure you're logged out. Then, click Sign in with SSO after entering the email associated with the domain of this new tenant you just configured. If prompted, you will need to click Accept:

Now, you will see your user in the sidebar, which means you added OIDC SSO successfully!

Adding SAML SSO with Descope and Azure
In order to create a new SAML app within Azure, you may want to create a new tenant. This is because domains are bound to the tenant, so you can't have users at different domains with the same tenant within Azure. Here's how to create a new tenant.
Navigate to https://portal.azure.com/#view/Microsoft_AAD_IAM/DirectorySwitchBlade/subtitle/ and create a new tenant to use for this example. Choose Microsoft Entra External Id under the Basics tab. Click on the Configuration tab and enter your organization and domain name.
This example uses descopeoidcsampletenant
as the organization and domain name. However, this name should relate to the organization you are configuring.

Click Create, and switch to the tenant in the top right. Now, you can create an application as before with the OIDC SSO implementation by going to your app registrations, except choose the third option titled Integrate any other application you don't find in the gallery (Non-gallery) under What are you looking to do with your application? after entering a name for your tenant.

Go back to Descope and create a new tenant as per the OIDC section above. Remember, you need to make a tenant for each separate source/grouping of users. In this example, the tenant represents your employees, and the other tenant you created in Descope earlier represents the users for one of your clients.
Go back to the SSO page in Descope after selecting your new tenant here, but enable SAML, not OIDC. Now, enter the domain for this tenant under SSO Domains within Tenant Details at the top of this page. Scroll down to SSO Configuration and check Enter the connection details manually. Here's what it will look like once you have entered all the information after the next few steps.

To enter the information pictured above, which will allow Descope to connect to Azure, head back to the app registration in Azure. Go to Manage > Users and Groups > + Add user/group. Then, click on the none selected option. Then, select whatever users you want assigned to this tenant.
Now, go to Manage > Single sign-on and choose SAML. Scroll down to section 4. Then, copy the values for Login Url and Microsoft Entra Identifier and paste them into their respective sections in Descope.
SSO (Single Sign-On) URL: Login Url
Entity ID: Microsoft Entra Identifier

Now, you need to enter the value for Certificate in Descope. Go back to the Single sign-on page in Azure, and click Download next to Certificate (Based64) in section 3 (see the arrow indicating this section in the above image). Open this file in a text editor. If you're using a Mac, you may need to head to Settings > Privacy and Security, scroll down to Security, and check Open Anyway. Then, copy the text and paste it into the Certificate field in Descope.

Finally, scroll down in Descope to the section under Service Provider. Go back to Azure, scroll back up to section 1 on this same page, and click on Edit.
For Identifier (Entity ID), paste in the value for Descope Entity ID (not required by all providers) from Descope.
For Reply URL (Assertion Consumer Service URL), paste in the value copied from Descope ACS URL in Descope.
SSO mapping for SAML SSO
The last thing you need to implement is SSO mapping so that Descope and the app can access user attributes like assigned groups, roles, name, and email. Unlike OIDC, the SAML protocol doesn't include a user info endpoint, so you'll need to map these attributes manually.
Go back to the Single sign-on page under Manage (where you were earlier) and click Edit under Attributes & Claims. You'll need to edit the first two claims under Additional Claims. For the email claim, click the claim with Claim name http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
. On the Manage Claim page, delete the Namespace field and change the Name field to emailaddress
.
For the display name claim, go back to the Attributes & Claims page and click the claim with Claim name http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname
. Delete the Namespace field, change the Name field to displayname, and change the Source attribute to user.displayname
.
Once you're done editing, the displayname
claim should look like this:

Here's what the Attributes & Claims page will look like after you've edited both additional claims:

Go back to Descope and scroll down on the same SSO page until you reach the SSO Mapping section. Under User Attribute Mapping, click + Add user attribute mapping and enter displayname
for the IdP user value and Display Name
for the Descope user attribute. Add a second mapping with emailaddress
for IdP user value and Email
for Descope user attribute.

You can now log in with SSO in the Remix app again, but make sure to use a user you've selected for this new tenant. If you see your name on the bottom left of the Remix app after logging in, you can confirm that the SSO mapping and other SAML configuration you entered was correct. Optionally, you can visit https://app.descope.com/users and edit a user to confirm it was assigned to the correct tenant after you logged in.
Conclusion
After reading this article, you should have a solid understanding of how and why you should use SSO, secure pages, and APIs in your Remix app. You've also learned how to set up app registrations in Azure for both OIDC and SAML and how to configure Remix to support OIDC, SAML, and magic link logins. You also briefly explored the Descope flow editor and how to start building more advanced authentication flows.
With Descope's B2B CIAM and SSO capabilities, you've seen how you can centralize and streamline authentication for your users while also managing roles and other authorization settings in one place. You get all this without having to implement complex protocols like SAML or OIDC yourself, saving you time, effort, and potential security concerns.
Sign up for a Descope Free Forever account to get started or book a demo to learn more about the platform.