😎 React App Leveled Up with Open-Source SSO Auth Wizardry 🪄
Tags: webdev, programming, tutorial, opensource
Published: 2023-11-02 13:56:16

## TL;DR

I'm Nathan and one thing I love is React development. 

By the end of this tutorial, you'll have a proper React app that will authenticate users by SAML Single Sign-On. 

Before we dive into the implementation details, make sure you have the following in place:

- A React app for enabling SAML Single Sign-On.
<br />

- An Express.js backend for our React App. You can find the code for this backend [here](https://github.com/boxyhq/jackson-examples/tree/main/apps/express-api).
<br />

- Knowledge of [UI best practices](https://boxyhq.com/guides/jackson/configuring-saml-sso#sso-connection-identifier) for configuring SAML Single Sign-On and the [SSO Connection API](https://boxyhq.com/docs/jackson/sso-flow/) (These concepts are essential for integrating SAML SSO into your app).
<br />

- Since we are using API's I would recommend using the open-source Postman alternative, [Firecamp](https://firecamp.io/) for testing your routes.
<br />
{% cta https://dub.sh/saml-jackson %}  Please star ⭐ Firecamp {% endcta %}
<br />


![Baby Yoda](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rw7iyjjjpjf636kydy1u.gif)



## Table of Contents

- [Overview of the Integration Steps](#overview-of-the-integration-steps)
- [Authenticate with SAML Single Sign On](#authenticate-with-saml-single-sign-on)
- [Deploy SAML Jackson](#deploy-saml-jackson)
- [Set Up SAML Jackson Integration](#set-up-saml-jackson-integration)
- [Set Up Global Authentication Primitives](#set-up-global-authentication-primitives)
- [AuthProvider](#authprovider)
- [Authentication Request](#authentication-request)
- [Fetch User Profile](#fetch-user-profile)
- [Sucess](#success)
- [Key Takeaways](#key-takeaways)
- [Why You Should Care About SSO](#why-you-should-care-about-sso)

## Why You Should Care About SSO

SAML SSO enables seamless and secure authentication, reducing the need for users to remember multiple passwords and minimizing the risk of password-related security breaches. 

It also allows enterprises to centrally manage user access and permissions, making it easier to control who has access to the app and what they can do within it.

By integrating SAML SSO, your app aligns with industry standards for authentication and access control, providing a trusted and efficient user experience while bolstering the app's security and administrative capabilities.


Now that we've got the prerequisite's covered, we can now begin this journey of enabling **SAML** Single Sign-On authentication for our users. 🎉
<hr />


_**I've mentioned SAML...you've probably heard of him? ❤️**_

[![SAML Jackson](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4fkc326fqxjkdudjektw.gif)](https://dub.sh/saml-jackson)
<br />

{% cta https://dub.sh/saml-jackson %}  Please star ⭐ the SAML Jackson repo {% endcta %}

<hr />

## Overview of the Integration Steps

Integrating SAML Single Sign-On into your React app involves the following key steps:

- **Configure SAML Single Sign-On:** Set up SAML Single Sign-On for your application.

- **Authenticate with SAML Single Sign-On:** Implement the authentication process with SAML Single Sign-On.

- **Configure Enterprise SSO on React:** Allow your tenants to configure SAML connections for their users.

Now, let's dive into the details of each step.

## Authenticate with SAML Single Sign On

Once you've added a SAML connection to your application, you can use it to initiate the SSO authentication flow using SAML Jackson. This section will focus on the SSO authentication process.

## Deploy SAML Jackson

To get started, deploy the SAML Jackson service. Follow the [deployment documentation](https://boxyhq.com/docs/jackson/deploy/service) to install and configure SAML Jackson.

## Set Up SAML Jackson Integration

We'll use the client library `@bity/oauth2-auth-code-pkce` to implement the authentication process. 

This library is a zero-dependency OAuth 2.0 client that implements the authorization code grant with PKCE for client-side protection. You can install it using npm:

```javascript
npm i --save @bity/oauth2-auth-code-pkce
```
Next, configure the `OAuth2AuthCodePKCE` client to use the SAML Jackson service for authentication. You can create a custom hook to use the `oauthclient` throughout your app. 

Let's start by creating this file:


```javascript
src/hooks/useOAuthClient.ts

import { OAuth2AuthCodePKCE } from '@bity/oauth2-auth-code-pkce';
import { useEffect, useState } from 'react';

const JACKSON_URL = process.env.REACT_APP_JACKSON_URL;

interface OauthClientOptions {
  redirectUrl: string;
}
export default function useOAuthClient({
  redirectUrl,
}: OauthClientOptions): OAuth2AuthCodePKCE | null {
  const [oauthClient, setOauthClient] = useState<OAuth2AuthCodePKCE | null>(
    null
  );

  useEffect(() => {
    setOauthClient(
      new OAuth2AuthCodePKCE({
        authorizationUrl: `${JACKSON_URL}/api/oauth/authorize`,
        tokenUrl: `${JACKSON_URL}/api/oauth/token`,
        // Setting the clientId dummy here. We pass additional query params for
        // tenant and product in the authorize request.
        clientId: 'dummy',
        redirectUrl,
        scopes: [],
        onAccessTokenExpiry(refreshAccessToken) {
          console.log('Expired! Access token needs to be renewed.');
          alert(
            'We will try to get a new access token via grant code or refresh token.'
          );
          return refreshAccessToken();
        },
        onInvalidGrant(refreshAuthCodeOrRefreshToken) {
          console.log(
            'Expired! Auth code or refresh token needs to be renewed.'
          );
          alert('Redirecting to auth server to obtain a new auth grant code.');
          //return refreshAuthCodeOrRefreshToken();
        },
      })
    );
  }, [redirectUrl]);

  return oauthClient;
}
```
## Set Up Global Authentication Primitives

Let me describe how we will use `AuthContext`.

To make the authentication process globally accessible throughout our app, we'll create an `AuthContext` that stores information about the logged-in `user`, as well as `signIn` and `signOut` methods.

These, along with the `setTenant` (method used to select the tenant for the SSO flow) and `authStatus` (boolean which helps us to conditionally render content based on whether the authenticated status is fully known or being loaded).

Let's create our next file: 

```javascript
src/lib/AuthProvider.tsx

import React, { useState, useEffect, ReactNode, createContext } from 'react';
import { useLocation } from 'react-router-dom';
import useOAuthClient from '../hooks/useOAuthClient';
import { authenticate, getProfileByJWT } from './backend';

interface ProviderProps {
  children: ReactNode;
}

interface AuthContextInterface {
  setTenant?: React.Dispatch<React.SetStateAction<string>>;
  authStatus: 'UNKNOWN' | 'FETCHING' | 'LOADED';
  user: any;
  signIn: () => void;
  signOut: (callback: VoidFunction) => void;
}

// localstorage key to store from url
const APP_FROM_URL = 'appFromUrl';

export const AuthContext = createContext<AuthContextInterface>(null!);
```

The next task at hand is creating a custom hook that returns a handle to the `AuthContext`.

Make a new file: 
```javascript
src/hooks/useAuth.ts

import { useContext } from 'react';
import { AuthContext } from '../lib/AuthProvider';

const useAuth = () => {
  return useContext(AuthContext);
};

export default useAuth;
```

## AuthProvider

Next, we'll wire up the flow inside the `AuthProvider`.

1. Once the app shell is rendered, a useEffect runs which conducts the flow  to `authClient` from `useOAuthClient`.

Two scenarios need to be handled here. 

-  We secure an `access_token` from the SSO provider (Jackson), which we retrieve the logged-in user profile by passing in the cookie. 

-  The browser gets redirected back to the app, after signing in at IdP. The authorization code in the redirect is exchanged for an access token which is then passed to the app backend to complete the login.

```javascript
src/lib/AuthProvider.tsx

  const AuthProvider = ({ children }: ProviderProps) => {
    const [user, setUser] = useState<any>(null);
    const [authStatus, setAuthStatus] = useState<AuthContextInterface['authStatus']>('UNKNOWN');

    ...

    const redirectUrl = process.env.REACT_APP_APP_URL + from;

    const authClient = useOAuthClient({ redirectUrl });

    useEffect(() => {
      let didCancel = false;

      const loadUser = async () => {
        if (!authClient) {
          return;
        }
        setAuthStatus('FETCHING');
        if (authClient.isAuthorized()) {
          const { data, error } = await getProfileByJWT();
          if (!didCancel && !error) {
            setUser(data);
            setAuthStatus('LOADED');
          }
        } else {
          try {
            const hasAuthCode = await authClient?.isReturningFromAuthServer();
            if (!hasAuthCode) {
              devLogger('no auth code detected...');
            } else {
              const token = !didCancel
                ? await authClient?.getAccessToken()
                : null;
              token && localStorage.removeItem(APP_FROM_URL);
              // authentication happens at the backend where the above token is used
              // to retrieve user profile
              const profile = await authenticate(token?.token?.value);
              if (!didCancel && profile) {
                setUser(profile);
              }
            }
          } catch (err) {
            console.error(err);
          } finally {
            setAuthStatus('LOADED');
          }
        }
      };

      loadUser();
      return () => {
        didCancel = true;
      };
    }, [authClient]);

    ...

     const value = {
      authStatus,
      user,
    };

    return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
  };

  export { AuthContext, AuthProvider };
```

2. When someone tries to access **protected/private** routes they will be `redirected` to the login page. 

Let's explain a little further. 

First we save the current location they were trying to access in the history state. 

This logic is encapsulated in the `RequireAuth` wrapper component. We'll use it to protect routes that require authentication.

```javascript
src/components/RequireAuth.tsx

const RequireAuth = ({ children }: { children: JSX.Element }) => {
  let { user, authStatus } = useAuth();
  let location = useLocation();

  if (authStatus !== 'LOADED') {
    return null;
  }

  if (!user) {
    // Redirect them to the /login page, but save the current location they were
    // trying to go to when they were redirected. This allows us to send them
    // along to that page after they login, which is a nicer user experience
    // than dropping them off on the home page.
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return children;
};

export default RequireAuth;
```

Next we will use the `from` state in the `redirectUrl` to construct the `oAuthClient` inside `AuthProvider`.

```javascript
src/lib/AuthProvider.tsx

let location = useLocation();
let from =
  location.state?.from?.pathname ||
  localStorage.getItem(APP_FROM_URL) ||
  '/profile';

const redirectUrl = process.env.REACT_APP_APP_URL + from;

const authClient = useOAuthClient({ redirectUrl });
```

3. `signIn` and `signOut` methods can be implemented as follows:

```javascript
src/lib/AuthProvider.tsx

const signIn = async () => {
  // store the 'from' url before redirecting ... we need this to correctly initialize
  // the oauthClient after getting redirected back from SSO Provider.
  localStorage.setItem(APP_FROM_URL, from);
  // Initiate the login flow
  await authClient?.fetchAuthorizationCode({
    tenant,
    product: 'saml-demo.boxyhq.com',
  });
};

const signOut = async (callback: VoidFunction) => {
  authClient?.reset();
  setUser(null);
  callback();
};

const value = {
  signIn,
  signOut,
};

return (
  <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
);
```

## Authentication Request

Let's add a page to begin the authenticate flow. 

This page initiates (by calling `signIn` from the `AuthContext`) the SAML SSO flow by redirecting the users to their configured _Identity Provider_ (**via Jackson**).

The user will be redirected to the IdP when clicking the "**Continue with SAML SSO**" button.

```javascript
src/pages/Login.tsx

import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import useAuth from '../hooks/useAuth';

const Login = () => {
  let location = useLocation();

  let from = location.state?.from?.pathname || '/profile';

  const { signIn, setTenant, authStatus, user } = useAuth();

  if (authStatus !== 'LOADED') {
    return null;
  }

  if (authStatus === 'LOADED' && user) {
    return <Navigate to={from} replace />;
  }

  return (
    <div className="mx-auto h-screen max-w-7xl">
      <div className="flex h-full flex-col justify-center space-y-5">
        <h2 className="text-center text-3xl">Log in to App</h2>
        <div className="mx-auto w-full max-w-md px-3 md:px-0">
          <div className="rounded border border-gray-200 bg-white py-5 px-5">
            <form className="space-y-3" method="POST" onSubmit={signIn}>
              <label htmlFor="tenant" className="block text-sm">
                Tenant ID
              </label>
              <input
                type="text"
                name="tenant"
                placeholder="boxyhq"
                defaultValue="boxyhq.com"
                className="block w-full appearance-none rounded border border-gray-300 text-sm placeholder-gray-400 focus:outline-none focus:ring-indigo-500"
                required
                onChange={(e) =>
                  typeof setTenant === 'function' && setTenant(e.target.value)
                }
              />
              <button
                type="submit"
                className="w-full rounded border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white focus:outline-none"
              >
                Continue with SAML SSO
              </button>
            </form>
          </div>
        </div>
      </div>
    </div>
  );
};

export default Login;
```

## Fetch User Profile

Once the `accessToken` has been fetched, the React app can use it to retrieve the user profile from the Identity Provider.

Typically you would use your backend service (**Eg: Express.js**) to call the **SAML Jackson API** to fetch the user profile using the `accessToken`.

Let's take a look at the **express.js** routes that return the user profile either on login or by parsing the JWT from the client-side cookie.

```javascript

app.get('/api/authenticate', async function (req, res, next) {
  const accessToken = req.query.access_token;

  if (!accessToken) {
    throw new Error('Access token not found.');
  }

  const response = await fetch(
    `${jacksonUrl}/api/oauth/userinfo?access_token=${accessToken}`,
    {
      method: 'GET',
    }
  );

  const profile = await response.json();

  // Once the user has been retrieved from the Identity Provider,
  // you may determine if the user exists in your application and authenticate the user.
  // If the user does not exist in your application, you will typically create a new record in your database to represent the user.

  const token = jsonwebtoken.sign(
    {
      id: profile.id,
      email: profile.email,
      firstName: profile.firstName,
      lastName: profile.lastName,
    },
    jwtSecret
  );

  res.cookie('sso-token', token, { httpOnly: true });
  res.json(profile);
});

app.get('/api/profile', async function (req, res, next) {
  const token = req.cookies['sso-token'];

  if (!token) {
    return res
      .status(401)
      .json({ data: null, error: { message: 'Missing JWT' } });
  }

  // You may fetch the user profile from your database using the user id.

  const payload = jsonwebtoken.verify(token, jwtSecret);

  return res.json({ data: payload, error: null });
});
```

You can check the terminal and see the returned profile in JSON.

```json
{
  "id":"<id from the Identity Provider>",
  "email": "jackson@coolstartup.com",
  "firstName": "SAML",
  "lastName": "Jackson",
  "requested": {
    "tenant": "<tenant>",
    "product": "<product>",
    "client_id": "<client_id>",
    "state": "<state>"
  },
  "raw": {
    ...
  }
}
```

In the React app, we call the `getProfileByJWT` if an access_token is already in possession or we call the `authenticate` when returning back from SSO provider with the authorization code.

```javascript
src/lib/backend.ts

const apiUrl = process.env.REACT_APP_API_URL;

export const authenticate = async (token: string | undefined) => {
  if (!token) {
    throw new Error('Access token not found.');
  }

  const response = await fetch(
    `${apiUrl}/api/authenticate?access_token=${token}`,
    {
      method: 'GET',
      credentials: 'include',
    }
  );
  if (response.ok) {
    return await response.json();
  }
  return null;
};

export const getProfileByJWT = async () => {
  const response = await fetch(`${apiUrl}/api/profile`, {
    method: 'GET',
    credentials: 'include',
  });

  return await response.json();
};
```

## Success

That's it, your React app is ready to handle Single Sign-On authentication. 🎉

## Key Takeaways

According to [statista](https://www.statista.com/statistics/1124699/worldwide-developer-survey-most-used-frameworks-web/), React is the second most popular framework used by developers at roughly 40%. 

The ability to create and implement SAML Single Sign-On (SSO) in your own deployed app is of paramount importance because it not only enhances security and user convenience but also streamlines access control for both users and administrators. 
<hr />
If you love open-source, please let me know in the comments or reach out to me on [Twitter (X)](https://twitter.com/nathan_tarbert) and tell me what you're building!