Skip to main contentArrow Right

Table of Contents

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:

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.

Fig: Screenshot of Descope dashboard showing the user selecting Magic Link and SSO as primary authentication methods on the Getting Started page
Fig: Descope dashboard showing the user selecting Magic Link and SSO as primary authentication methods on the Getting Started page

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.

Fig: Screenshot of the Descope Flows dashboard showing the user selecting the Import flow button
Fig: Descope Flows dashboard showing the user selecting the Import flow button

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.

Fig: The Remix application login screen saying "Welcome to the RemixOIDC/SAML SSO Sample App", with a Send Magic Link button and Continue with SSO button
Fig: The Remix application login screen saying "Welcome to the RemixOIDC/SAML SSO Sample App", with a Send Magic Link button and Continue with SSO button

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?

Fig: Screenshot of the Create your own application page in Azure with the user selecting Register an application to integrate with Microsoft Entra ID (App you're developing)
Fig: The Create your own application page in Azure with the user selecting Register an application to integrate with Microsoft Entra ID (App you're developing)

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.

Fig: Screenshot of entering https://api.descope.com/oauth2/v1/logout into the front-channel logout URL field in Azure
Fig: Entering https://api.descope.com/oauth2/v1/logout into the front-channel logout URL field in Azure

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.

Fig: Screenshot of the SSO page for the newly created tenant in Descope
Fig: The SSO page for the newly created tenant in Descope

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 file

  • User 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:

Fig: Permissions requested dialog when signing in with Azure account
Fig: Permissions requested dialog when signing in with Azure account

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

Fig: The username and email displayed in the nav bar in the Remix application
Fig: The username and email displayed in the nav bar in the Remix application

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.

Fig: The Configuration tab in the Create a Tenant page in Azure
Fig: The Configuration tab in the Create a Tenant page in Azure

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.

Fig: Selecting the option "Integrate any other application you don't find in the gallery Non-gallery" in Azure when creating a new application
Fig: Selecting the option "Integrate any other application you don't find in the gallery Non-gallery" in Azure when creating a new application

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.

Fig: The completed Enter connection details manually section under SSO configuration—with SSO URL, entity ID, and certificate entered
Fig: The completed "Enter connection details manually" section under SSO configuration—with SSO URL, entity ID, and certificate entered

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

Fig: Entire SAML SSO configuration page with arrows showing location of Login Url, Microsoft Entra Identifier, and Certificate
Fig: Entire SAML SSO configuration page with arrows showing location of Login Url, Microsoft Entra Identifier, and Certificate

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.

Fig: "Open Anyway" in Mac under Privacy and Security
Fig: "Open Anyway" in Mac under Privacy and Security

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:

Fig: The Manage Claim page for the edited displayname claim
Fig: The Manage Claim page for the edited displayname claim

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

Fig: The Attributes & Claims page showing the edited displayname and emailaddress claims
Fig: The Attributes & Claims page showing the edited displayname and emailaddress 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.

Fig: Final User Attribute Mapping under SSO Mapping
Fig: Final User Attribute Mapping under SSO Mapping

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.