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
>
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).