Broadleaf Microservices
  • v1.0.0-latest-prod

Auth React SDK

The Broadleaf Auth React SDK is designed to make interacting with the Broadleaf Authorization Server easier for a single-page React application.

Installation

This library can be installed using Yarn or NPM:

Yarn

yarn add @broadleaf/auth-react

NPM

npm install @broadleaf/auth-react

Getting Started

Setting Up the Provider

Start by configuring the AuthProvider as a wrapper around your App:

import React from 'react';
import ReactDOM from 'react-dom';
import { AuthProvider } from '@broadleaf/auth-react';
import App from './App';

ReactDOM.render(
  <AuthProvider
    baseURL="/auth"
    clientId="my-app"
  >
    <App/>
  </AuthProvider>,
  document.getElementById('root')
);

Now, you can use the useAuth hook to access the provider state and methods:

import React from 'react';
import { useAuth } from '@broadleaf/auth-react';

const App = () => {
  const {
    error,
    isAuthenticated,
    isLoading,
    loginWithRedirect,
    logoutWithRedirect,
    user
  } = useAuth();

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>{error.message}</div>;
  }

  if (!isAuthenticated) {
    return <button onClick={() => loginWithRedirect()}>Login</button>
  }

  return (
    <div>
      <h1>Welcome {user.fullName}</h1>
      <button onClick={() => logoutWithRedirect()}>Logout</button>
    </div>
  );
};

export default App;

Securing API Calls

If you want to secure calls to your APIs, you can do so using getAccessToken:

import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { useAuth } from '@broadleaf/auth-react';

const OrderList = () => {
  const { getAccessToken } = useAuth();
  const [orders, setOrders] = useState();

  useEffect(() => {
    (async () => {
      try {
        const accessToken = await getAccessToken();
        const response = await fetch('/api/orders', {
          headers: {
            Authorization: `Bearer ${accessToken}`
          }
        });

        setOrders(await response.json());
      } catch (e) {
        console.error('Unable to fetch orders', e);
      }
    })();
  }, [getAccessToken, isAuthenticated])

  if (!orders) {
    return <div>Loading Orders...</div>;
  }

  return (
    <div>
      {orders.map(order => <Link to={`/orders/${order.orderNumber}`}>{order.orderNumber}</Link>)}
    </div>
  )
}

export default OrderList;

Supporting Silent Authentication Flow

By default and as a fallback for Refresh Token Rotation, getAccessToken uses a hidden iframe to execute the OAuth2 Authorization Code Grant with PKCE to retrieve tokens in the background. In order to support this flow, we need our application to serve a special callback URL to communicate the query parameters returned by the authorization flow. We provide an example file as part of the installed library, which can be found at node_modules/@broadeaf/auth-web/example/silent-callback.html:

<!DOCTYPE html>
<html>
  <head>
    <script type="text/javascript">
      parent.postMessage(
        { type: 'authorization_code', search: window.location.search },
        window.location.origin
      );
    </script>
  </head>
  <body></body>
</html>

Each environment may have a different way of providing this file on the silent callback route. Be sure that your various environments are capable of serving this static resource at default silentRedirectUri at ${window.location.origin}/silent-callback.html. If you want to serve this file at a custom URL, you can configure the silentRedirectUri when rendering the AuthProvider:

import React from 'react';
import ReactDOM from 'react-dom';
import { AuthProvider } from '@broadleaf/auth-react';
import App from './App';

ReactDOM.render(
  <AuthProvider
    baseURL="/auth"
    clientId="my-app"
    silentRedirectUri={`${window.location.origin}/custom-silent-callback.html`}
  >
    <App/>
  </AuthProvider>,
  document.getElementById('root')
);

Securing Routes

If you are using a router library such as react-router-dom, you will need to pass a custom onRedirectCallback function to handle the post-login redirect:

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, useHistory } from 'react-router-dom';
import { AuthProvider } from '@broadleaf/auth-react';
import App from './App';

const AppWithAuth = () => {
  const history = useHistory();
  return (
    <AuthProvider
      baseURL="/auth"
      clientId="my-app"
      onRedirectCallback={appState => {
        history.replace(appState?.returnTo || window.location.pathname)
      }}
      >
      <App/>
    </AuthProvider>
  );
};

ReactDOM.render(
  <BrowserRouter>
    <AppWithAuth/>
  </BrowserRouter>,
  document.getElementById('root')
);

If you want to secure a private route so that authentication is required to enter:

import React, { useEffect } from 'react';
import { Route } from 'react-router-dom';
import { useAuth } from '@broadleaf/auth-react';

const PrivateRoute = ({children, ...routeProps}) => {
  const { isAuthenticated } = useAuth();
  return (
    <Route {...routeProps}>
      {({ location }) => isAuthenticated ? children : <RedirectToLogin returnTo={location.pathname}/>}
    </Route>
  )
};

const RedirectToLogin = ({ returnTo }) => {
  const { loginWithRedirect } = useAuth();
  useEffect(
    () => {
      loginWithRedirect({ appState: { returnTo } });
    },
    []
  );
  return null;
};

export default PrivateRoute;

Accessing User Profile

If you want to display or otherwise utilize user profile information within your application, use the useAuth hook:

import { useAuth } from '@broadleaf/auth-react';

const Welcome = () => {
  const { isAuthenticated, user } = useAuth();
  if (!isAuthenticated) {
    return <div>{'Welcome Guest!'}</div>;
  }
  return <div>{`Welcome, ${user.full_name}!`}</div>
};

export default Header;

Session Inactivity

If you want to implement a session inactivity timer or keep-alive behavior, it is best to use the useAuth hook to access the current state, or check the user’s session:

function useKeepSessionAlive(isWindowFocused = false, showInactivityNotice) {
  const {
    checkSession,
    isAuthenticated,
    logoutWithRedirect,
    session
  } = useAuth();

  useInterval(
    checkSession,
    // if the window is focused, refresh the session timer every 5 minutes
    isWindowFocused && isAuthenticated
      ? 1000 * 60 * 5
      : null
  );

  useInterval(
    () => {
      const inactivityDate = asDate(session.inactivityAt);
      const timeLeftInSession = Math.max(
        inactivityDate.getTime() - Date.now(),
        0
      );
      if (timeLeftInSession <= 0) {
        logoutWithRedirect();
      } else if (timeLeftInSession <= 1000 * 60 * 3) {
        showInactivityNotice();
      }
    },
    isAuthenticated && session?.inactivityAt ? 1000 : null
  );
}

Additional Support for Cross-Origin Auth

There may be times when the frontend app using the SDK is hosted on a different domain than the Auth Server rather than hosting both behind a proxy gateway. In this case, the BLSID cookie will be considered a third-party cookie and blocked by browsers by default.

To handle auth in this case, refresh-token-rotation should be enabled. Along with that, because automatically logging in users after they register or reset their password relies on the session cookie as well, there is a parameter that can be added to the redirect URIs used to indicate to the AuthProvider that it should automatically call loginWithRedirect when detected to give a more seamless experience to users.

To enable these features, you would set up your AuthProvider like the following:

+

<AuthProvider
  key={clientId}
  accountId={accountId}
  baseURL={https://www.my-auth-server.com}
  redirectUri={https://www.my-frontend.com}
  clientId={clientId}
  credentials
  scope="USER CUSTOMER"
  useRefreshTokens
  usePkce
  useIFrameAsFallbackForRefreshToken={false}
  useSessionStorageForTokenCache // Session storage should be cleared when the browse closes
  useLocalStorageForTokenCache // local storage won't be
>

The refresh-token-rotation will issue an initial refresh token that will be used to obtain access tokens for as long as it is valid. Eventually, when the refresh token expires, any access token requests will be rejected with an invalid_grant error.

As of version 1.6.3, such errors will be handled by automatically redirecting the user to the /authorize endpoint. This is done via the loginWithRedirect() method in an attempt to get a new refresh token.

  • In the event that the session token (BLSID) is still valid at this time, the /authorize request will succeed naturally and no login flow will be engaged.

  • In the event that the session token (BLSID) is invalid/expired at this time, the /authorize request will fail and redirect to /login.

Tip
Visit this documentation to learn more about the refresh-token-rotation.
Tip
The Authorization Server (managed in the Admin) should allow cross-origin SSO and the Authorized Client should allow the refresh_token grant type (under Advanced).

Remember-Me Login

(since 1.6.3)

Tip
This functionality works with Universal Login only.

The Remember-Me Login functionality can be leveraged in a front-end project using both cross-origin and non-cross origin auth.

Cross-Origin Setup

In a cross-origin setup, we will rely on the refresh-token-rotation to issue new access tokens. Upon expiration, the user will be redirected to /authorize and may ultimately be redirected to /login, as explained in the cross-origin support section.

At this stage, if a remember-me cookie (BLRM) is available, the user will automatically be signed in. Otherwise, the user will be presented with a form and will have to manually enter their credentials

Non-Cross-Origin Setup

In a non-cross-origin setup, you may want to leverage the redirectToRememberMeUrl method and the session information that is returned from the useAuth hook. Visit the Auth Web documentation for a simple example on how to use this.

  • The redirectToRememberMeUrl method will redirect the user to the /remember-me-continuation endpoint, which ultimately redirects the user to /login, where they will be signed in automatically if a remember-me cookie is available.

  • The session object will contain a inactivityAt date that indicates when the current session will expire, and a rememberMeAvailable boolean denoting whether a remember-me cookie is available.

You may want to use this information to call the redirectToRememberMe method when the end of session is reached, or to proactively prompt the user to stay signed in before this date is reached.