Broadleaf Microservices
  • v1.0.0-latest-prod

Authentication & Authorization Walkthrough

In this walkthrough, we’ll be examining how we handle authentication and authorization in the commerce starter frontend. This walkthrough will look at the components used in the starter as well as the common API calls.

Setup for Handling Authorization

First, we need to know how we are going to handle getting authorization for the app to hit microservices APIs. We need to get an OAuth access token after the user authenticates, either immediately after logging in or using an existing session. In this section, we’ll cover the additional setup for two ways of getting an access token:

Silent Authorization

In the commerce app, we use the standard OAuth Authorization Code Grant Flow with Proof-Key-for-Code-Exchange and renew tokens using Silent Auth—see Authorization docs for details on what that looks like. Silent Auth means that the authorization requests take place in an iframe so that the user is not forced to redirect back to auth every time their access token expires.

To support Silent Auth, we define a silent-callback.html page that the Auth Server can redirect to inside an iframe. This loads a simple JavaScript file to post the authorization code to the iframe’s parent window.

Tip
Also make sure to add silent-callback.html to you Authorized Client’s redirect URIs, which should be handled: https://www.my-app.com/silent-callback.html.
silent-callback.html
<!DOCTYPE html>
<html>
  <head>
    <script type="text/javascript" src="/silent-callback.js"></script>
  </head>
  <body></body>
</html>
silent-callback.js
parent.postMessage(
  { type: 'authorization_code', search: window.location.search },
  window.location.origin
);

Once done, we can move on to Establishing an Authentication Context.

Refresh Token Rotation

Silent Authorization is not always feasible such as when the commerce app is hosted on a different domain than the microservices since most modern browsers block cookies with separate domains. This means that using an iframe to redirect to the Auth Server to get a new token will not work—it requires a cookie set by Auth after login to be present, but Auth is on a separate domain. In this case, we need to enable Refresh Token Rotation. In this case, we use a second token to refresh the access token rather than doing a redirect in an iframe. When the new access token is returned, we get a new refresh token, and the old one is invalidated, hence rotation.

With this, there is no need for a silent-callback.html page, but we do have to update the configuration of the Auth Server as discussed in Configuring Refresh Tokens. Once done, we can move on to Establishing an Authentication Context.

Establishing an Authentication Context

Before we can access the microservice APIs, we should determine whether there is an authenticated session ongoing for the current user. Then, we need an access token to interact with APIs. It will also be helpful to components to know some additional state information about whether the authentication check is ongoing or has been attempted already.

Important
Before anything else, however, we do need to resolve the tenant and application, which is covered in [Tenant & Application Walkthrough]. Make sure to review that first.

Auth SDK Overview

The first step is to familiarize ourselves with the Auth JavaScript SDK that Broadleaf provides to facilitate a frontend application interacting with the Auth Microservice.

There are two modules in the Auth SDK:

  1. @broadleaf/auth-web: A JavaScript library built with TypeScript that can be used with any Node.js app regardless of framework, e.g., with React, Angular, Vue, etc. Also see the Auth Web SDK reference docs.

  2. @broadleaf/auth-react: A React-specific library that sets up a common React context to handle core authentication concerns for React apps, focused on client-side interactions. Also see the Auth React SDK reference docs.

The commerce starter that Broadleaf provides to new users already includes these and can be referenced by those implementing their own apps. Check under node_modules/@broadleaf/auth-[web|react]/src.

Auth SDKs in Node Modules
Figure 1. Auth SDKs inside of node_modules

Using Auth Web SDK

The @broadleaf/auth-web library provides types for requests and response payloads and a "client" for making requests against Broadleaf’s Auth Service s APIs to log in, register, log out, or fetch an access token according to the OAuth 2.0 specification for the Authorization Code Grant Flow. It can be configured to use an iframe for silent auth, enable the Proof-Key-for-Code-Exchange (PKCE) extension to the Authorization Code Grant Flow, or use refresh token rotation.

It also provides a basic implementation of a token cache to store OAuth access tokens for use in API requests that relies on either an in-memory map or browser storage if so configured. This can be used as-is or replaced with your own implementation of TokenCache by passing in a new instance to the constructor of AuthClient.

Note
It is not recommended to use browser-storage unless using refresh token rotation.

The Auth React SDK reference docs cover more details about the AuthClient. It is highly recommended to review these.

Checking the User’s Session

Tip
If using React, jump to Using Auth React SDK.
  1. Set up the AuthClient instance

    • See for the AuthClient configuration options.

      import { AuthClient } from '@broadleaf/auth-web';
      
      function initializeClient(clientOptions = {}) { (1)
        const defaultOptions = {
          baseURL: '/auth',
          clientId: 'my-authorized-client-id'
        };
        return new AuthClient({ ...defaultOptions, ...clientOptions });
      }
      
      export const AuthClient = initializeClient();
      1. clientOptions of type AuthClientOptions.

  2. Check session and initialize auth state for the app.

    1. Check for OAuth redirect parameters in case we have just been redirected from a successful log in

      • These params are code, state, and error.

    2. If present, then handle post-login actions such as redirecting to the homepage or the last page viewed, e.g., My Account Dashboard. This should use AuthClient#handleRedirectCallback

      • handleRedirectCallback assumes that login was initiated using #loginWithRedirect. It will return any appState properties cached when #loginWithRedirect was called. In the commerce starter for instance, we cache a returnTo URI that records where the user was before redirecting to log in. Thus, appState is any arbitrary data.

    3. If no params, check for an existing session by checking for an access token. This should use AuthClient#checkSession

      1. checkSession will check for a cached token or else try to fetch one.

      2. If not in the cache and silent-auth is enabled (the default), then an iframe will be created, and it’s src set to the authorization endpoint URL:

        Authorization URL Format
        GET '{baseUrl}/oauth/authorize?' \ (1)
          -d accountId={accountId} \ (2)
          -d client_id={clientId} \ (3)
          -d code_challenge={codeChallenge} \ (4)
          -d prompt='none' \ (5)
          -d redirect_uri={redirectUri} \ (6)
          -d response_type='code' \ (7)
          -d scope={scope} \ (8)
          -d state={state} (9)
        1. baseUrl will be the path and/or host to the Auth Service, e.g., /auth

        2. accountId is the ID of the customer’s selected B2B Account if any. This can affect a user’s roles and permissions in the commerce context.

        3. clientId is the ID of the commerce app’s Authorized Client

        4. codeChallenge will be generated by the AuthClient as part of PKCE.

        5. prompt='none' since the call is made from a hidden iframe.

        6. redirectUri will be the URI that Auth should redirect back to on success. Likely to just be the current origin. Must be whitelisted by the Authorized Client.

        7. response_type='code' indicates we want an authorization code in the response that we exchange for an access token.

        8. scope is the set of security scopes for which the user is requested access, e.g., USER CUSTOMER.

        9. state is a random string used to help verify the response from Auth is actually the one the app is expecting.

      3. Once Auth redirects back to silent-callback.html and the code is posted to the parent window, checkSession will exchange the authorization code for an access token using the token endpoint.

        POST '{baseUrl}/oauth/token?' \
          -H "Content-Type: application/x-www-form-urlencoded"
          -d accountId={accountId}
          -d client_id={clientId}
          -d code={authorizationCode}
          -d code_verifier={codeVerifier} (1)
          -d grant_type=authorization_code
          -d purpose= (2)
          -d redirect_uri={redirectUri}
          -d refresh_token= (3)
          -d scope={scope}
          -d username= (4)
        1. code_verifier is auto-generated by the Auth Client as part of PKCE.

        2. purpose is used with embedded login, in which case it would be OTP for one-time-password. More on embedded login later.

        3. username is used with embedded login, in which case it would be the user’s username.

      4. Then the token is cached.

      5. If auth responds with a login_required error, checkSession will swallow it as we will just set our app’s isAuthenticated state to false. It’s not necessary to force users to redirect to log in, we are simply checking if the session exists already.

Example Auth State Initialization
import qs from 'query-string';
import { AuthClient } from './auth-client';

function hasLoginRedirectParams(
  search = window.location.search
) {
  const { code, state, error } = qs.parse(search);
  return (!!code && !!state) || !!error;
}

function onRedirectCallback(appState) {
  window.history.replaceState(
    {},
    document.title,
    appState?.returnTo || window.location.pathname
  );
}

export async function getInitialAuthState() {
  if (hasLoginRedirectParams()) {
    // type appState will be any data supplied originally to the AuthClient#loginWithRedirect call
    // in this case, it will have a `returnTo` URI so the app knows where to redirect back to if it
    // came from a different page before logging in like a My Account page.
    const { appState } = await AuthClient.handleRedirectCallback(); (1)
    onRedirectCallback(appState);
  } else {
    // fetches a token if possible
    // swallows the login_required error, will handle that below instead
    await AuthClient.checkSession(); (2)
  }
  let initState;
  if (AuthClient.isAuthenticated()) {
    initState = {
      isAuthenticated: true,
      session: AuthClient.getSessionExpiry(), (3)
      user: AuthClient.getIdentityClaims() (4)
    };
  } else {
    initState = {
      isAuthenticated: false
    };
  }
  return initState;
}

Now we know if the user is authenticated and have even refreshed their access token. We can use this initialized state in our app.

Using Auth React SDK

In the @broadleaf/auth-react SDK, we have set up an AuthProvider component that sets up a common React context to handle core authentication concerns. It instantiates and manages an AuthClient with the provided configuration options from the caller. Then it exposes a number of callbacks that wrap methods of the AuthClient instance, so that the client itself is not exposed while still being usable by the app. We want to avoid unnecessary mutations and ensure there is only a single instance for the app.

It also manages some basic state like whether the authentication check has occurred, whether the user is authenticated, or whether the check is ongoing. To initialize state, the AuthProvider will check for URL params indicating that the app has just been redirected from a successful login attempt in the Auth Service or else check for a pre-existing session.

Example initialization of state based on AuthProvider internals.
useEffect(
  () => {
    // whether the request was cancelled due to the component unmounting
    let isCancelled = false;

    (async () => {
      try {
        // hasLoginRedirectParams() described in next code snippet
        if (hasLoginRedirectParams() && !skipRedirectCallback) {
          // Handles parsing the callback parameters as part of the OAuth2
          // Authorization Code Grant flow. This flow is typically first initiated
          // with a call to `AuthClient#loginWithRedirect`.
          const { appState } = await client.handleRedirectCallback();
          onRedirectCallback(appState);
        } else {
          // Checks if the user has a session and refreshes the session by silently
          // retrieving a token that pre-fills the token cache.
          await client.checkSession();
        }

        let initOptions;
        // check the token cache in the AuthClient to see if there's a token after the calls above
        if (client.isAuthenticated()) {
          initOptions = {
            isAuthenticated: true,
            session: client.getSessionExpiry(),
            user: client.getIdentityClaims()
          };
        } else {
          initOptions = {
            isAuthenticated: false
          };
        }
        !isCancelled && dispatch(initAction(initOptions));
      } catch (e) {
        !isCancelled && dispatch(errorAction(e));
      }
    })();

    return () => {
      isCancelled = true;
    };
  },
  // eslint-disable-next-line
  []
);
Utility to check for login-with-redirect params
import qs from 'query-string';

function hasLoginRedirectParams(
  search: string = window.location.search
): boolean {
  const { code, state, error } = qs.parse(search);
  return (!!code && !!state) || !!error;
}
Tip
More details on using the Auth React SDK can be found in the Auth React SDK reference docs. Additionally, the code can be viewed by adding @broadleaf/auth-react to your project.

Using Auth React SDK in the Commerce Starter

In the commerce starter, the AuthProvider component from the Auth React SDK is further augmented for the app’s specific needs, and can be seen in app/auth/contexts/auth-context.tsx. The primary points of interest in this component are how we are determining the values of various AuthClient configuration options by using environment variables to allow users of the starter to connect with their Auth microservice, use refresh token rotation, etc. without getting into the code to make changes. This is particularly useful for changing the configuration when the app is deployed to different environments.

Example Usage of AuthProvider
const ProviderWrapper = ({
  accountId,
  baseUrl,
  silentRedirectUri,
  clientId,
  router,
  useRefreshTokens,
  usePkce,
  userScope,
  useRefreshTokensWithLocalStorage,
  useRefreshTokensWithSessionStorage,
  fetchUserOperationsWithAccessToken
}) => (
  <AuthProvider
    key={clientId}
    accountId={accountId} // customer B2B account if any
    baseURL={baseUrl} // e.g., /auth
    redirectUri={baseUrl.href} // e.g., https://www.my-app.com/
    silentRedirectUri={silentRedirectUri.href} // e.g., https://www.my-app.com/silent-callback.html
    clientId={clientId} // authorized client id
    credentials // indicates whether cross-site Access-Control requests should be made using credentials
    onRedirectCallback={appState => {
      router.replace(appState?.returnTo || window.location.pathname);
    }}
    scope={userScope} // security scopes to use when requesting an access token, e.g., USER CUSTOMER
    useRefreshTokens={useRefreshTokens} // whether the app should use refresh tokens
    usePkce={usePkce} // whether to use Proof-Key-for-Code-Exchange (highly recommended)
    useIFrameAsFallbackForRefreshToken={
      !useRefreshTokensWithLocalStorage &&
      !useRefreshTokensWithSessionStorage
    }
    useSessionStorageForTokenCache={useRefreshTokensWithSessionStorage}
    useLocalStorageForTokenCache={useRefreshTokensWithLocalStorage}
    /*
     * If enabled, then user operations will be fetched using the 'resource API' user operations
     * endpoint that accepts a bearer token. Otherwise, user operations will be fetched using the
     * endpoint that relies on session cookie authentication. The access-token approach is useful
     * when in a cross-domain environment that can't pass session cookies.
     */
    fetchUserOperationsWithAccessToken={fetchUserOperationsWithAccessToken}
  >
    {/* component that handles keeping the session alive or else logging the user out when session expires */}
    <AuthSessionManager>{children}</AuthSessionManager>
  </AuthProvider>
);

Using the Auth Context

After establishing an Auth Context, we can look at some examples of how to use it to provide access to Broadleaf APIs. To access the context we will use the useAuth from @broadleaf/auth-react. The following logout button provides a simple example of the hooks use.

Tip
See AuthContext in the Auth React SDK reference docs for details on what is available from this hook.
Example: Basic Logout Button Component
import { useAuth } from '@broadleaf/auth-react';

export const LogoutButton = () => {
  const { logoutWithRedirect } = useAuth();

  return (
    <button
      onClick={() => {
        logoutWithRedirect();
      }}
      type="button"
    >
      Log Out
    </button>
  );
}

Managing Access Tokens

One useful hook in the starter that users will likely use or copy is the useGetCustomerAccessToken hook. This produces a callback method to fetch a new or cached access token through the AuthClient on demand. It will handle passing in the appropriate scopes for a customer or CSR user as configured for the app.

The hook is provided below and can be used as a basis for similar functions in frameworks other than React.

Based on app/auth/hooks/use-get-customer-access-token.ts
import { useCallback } from 'react';
import { useAuth } from '@broadleaf/auth-react';

import { useCsrContext } from '@src/csr/context';
import { CSR_SCOPE, isCsr } from '@src/csr/utils';

export const useGetCustomerAccessToken = () => {
  const { isAuthenticated, getAccessToken, user } = useAuth();
  const { csrAnonymous } = useCsrContext();

  return useCallback(
    async (options = {}) => { (1)
      try {
        if (!isAuthenticated || csrAnonymous) {
          // guest users will use the Commerce Gateway's token rather than their own
          // Commerce Gateway will detect that it should substitute its own when proxying requests
          return null;
        }

        const scopes = options.scope;
        if (isCsr(user) && (!scopes || scopes?.indexOf(CSR_SCOPE) === -1)) {
          const scope = `${CSR_SCOPE} ${options.scope || ''}`.trimEnd();
          options = { ...options, scope };
        }

        return await getAccessToken(options);
      } catch (error) {
        return null;
      }
    },
    [isAuthenticated]
  );
};
  1. options are of type GetAccessTokenOptions

The starter uses this when making any request from the client that may depend on user-specific data.

Example: Fetching an ItemList that stores wish-listed items.
import { ItemListClient } from '@broadleaf/commerce-cart';

import { useGetCustomerAccessToken } from '@app/auth/hooks';
import { useItemListClient } from '@src/common/contexts';

const useGetItemListsByName = (itemListName, options) => { (1)
  const getCustomerAccessToken = useGetCustomerAccessToken();
  const itemListClient = useItemListClient();
  return async () => {
      const accessToken = await getCustomerAccessToken();
      return itemListClient.listItemLists(`name==${itemListName}`, {accessToken});
  };
}
  1. options of type ClientOptions

Managing User B2B Account

The starter itself manages the user’s selected B2B Account and passes that into the AuthProvider so that it is included in token requests. This can impact what roles and permissions the user has specific to that Account.

Tip
See Shared Concepts: Accounts for more information on them.

The management of the selected Account is abstracted out of the Auth SDK since it can be handled in a number of different ways by the app. In the starter, we have a selector component in the header that lists all the accounts the user is a part of, allowing them to select which context they should shop or act within.

Account Selector
Account Selector Open to Show Options
Tip
Checkout app/layout/components/header/account-selector-drop-down.tsx for its details.

Accounts can be retrieved using the AccountClient#getCustomersAccounts from the Customer JS SDK after the user logs in.

Example hook to fetch accounts
// if using the commerce starter
import { useGetCustomerAccessToken } from '@src/auth/hooks';
import { useAccountClient } from '@src/common/contexts';

const useGetCustomerAccounts = () => {
  const getCustomerToken = useGetCustomerAccessToken();
  const accountClient = useAccountClient();
  return async () => {
    const accessToken = await getCustomerToken();
    return await accountClient.getCustomersAccounts({ accessToken });
  }
}

Once selected, we then cache the account ID in the app (either using browser storage or a call to the node server for a server-side cache) and trigger the app to reload. The ApplicationAuthProvider that wraps AuthProvider checks the cache and passes in the selected account ID to AuthProvider. Afterwards, the account ID can be found as a claim on the access token:

const { user } = useAuth();
const accountId = user?.acct_id;

Securing Components

Some components should only be visible to authenticated users such as my-account pages. Once an Auth Context is set up, we simply need to consult it to know whether the user is authenticated.

const Dashboard = () => {
  const { isAuthenticated, didAuthenticationCheck } = useAuth();

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

  if (!isAuthenticated) {
    window.location.replace('/login');
    return null;
  }

  return <div>Welcome to Your Account</div>
}

Handling Customer Login and Registration

Broadleaf supports customer login and registration by two methods:

  1. Universal Login: The user will be redirected to the Auth Server which serves its own login form. This is a more secure option.

  2. [Embedded Login with One-Time Passcode]: The commerce app hosts the login form so there is no redirect.

Tip
Broadleaf recommends Universal Registration (and Login) as the more secure option. Details can be found in Embedded Login with One-Time Password.

This section will cover both methods from the perspective of the commerce app.

Universal Login

When the login and registration forms are hosted directly by the Auth Server, the user will need to be redirected there, and the commerce app will need to handle when the user is redirected back to acquire an access token for API requests.

Login with Redirect

Using the Auth Web SDK makes this quite simple for implementors. To redirect to auth, call AuthClient#loginWithRedirect.

  1. This method will build a URL to hit the authorization endpoint:

    Authorization URL Format
    GET '{baseUrl}/oauth/authorize?' \ (1)
      -d accountId={accountId} \ (2)
      -d client_id={clientId} \ (3)
      -d code_challenge={codeChallenge} \ (4)
      -d code_challenge_method=S256
      -d redirect_uri={redirectUri} \ (5)
      -d response_type=code \ (6)
      -d scope={scope} \ (7)
      -d state={state} (8)
    1. baseUrl will be the path and/or host to the Auth Service, e.g., /auth

    2. accountId is the ID of the customer’s selected B2B Account if any. This can affect a user’s roles and permissions in the commerce context.

    3. clientId is the ID of the commerce app’s Authorized Client

    4. codeChallenge will be generated by the AuthClient as part of PKCE.

    5. redirectUri will be the URI that Auth should redirect back to on success. Likely to just be the current origin. Must be whitelisted by the Authorized Client.

    6. response_type='code' indicates we want an authorization code in the response that we exchange for an access token.

    7. scope is the set of security scopes for which the user is requested access, e.g., USER CUSTOMER.

    8. state is a random string used to help verify the response from Auth is actually the one the app is expecting.

  2. It will cache an auth transaction with the data included in the request in order to verify the response.

  3. Then it will redirect the browser to the authorization endpoint.

  4. Once Auth redirects back, the AuthProvider will check for the redirect params in the URL

Example log in button in React, but you could also import your AuthClient instance if using another framework as shown above.
import { useAuth } from '@broadleaf/auth-react';

const Login = () => {
  const { loginWithRedirect } = useAuth();
  return (
    <button
      onClick={() => {
        loginWithRedirect();
      }}
      type="button"
    >
      Log In
    </button>
  );
};

Registration with Redirect

Similarly to login, the Auth SDK makes registration simple for users with AuthClient#registerWithRedirect. This will simply redirect the browser to the Auth-hosted registration page providing the Authorized Client’s ID and a URI to redirect to.

Successful registration may automatically log in the user, depending on how the Authorization Server is configured. In which case, the AuthProvider will be able to get a new access token automatically. Otherwise, the user should be redirected to the login form.

Embedded Login with One-Time Password

In this section we will cover setting up registration and login forms hosted by the commerce app. We will cover the payloads, endpoints to use, and responses.

Tip
On the backend, broadleaf.auth.login.embedded.enabled (global property) must be true and the auth server being submitted to must have embeddedLoginEnabled also set to true.

Register with Credentials

Tip
In the commerce starter, you can view an example registration form in app/auth/components/embedded-register.tsx.

The registration form should have the following fields

  • userType: Hidden field that can default to CUSTOMER (as opposed to ADMIN) since this is the commerce app not admin.

  • fullName: The user’s full name in one field.

  • email: The user’s email.

  • username: Optionally the user’s chosen username if different from the email.

  • password: The user’s password. Must conform to whatever the business requirements for format are.

  • passwordConfirmation: The confirmation for verifying the password is not mistyped. Optional.

  • preview: Whether this is in a preview-on-site context and thus not a production user registration.

  • attributes.*: Client implementations may choose to utilize this field to accept and pass additional custom information as part of the user registration process.

This data should then be passed to AuthClient#registerWithCredentials.

registerWithCredentials will submit the data to {baseUrl}/register/embedded/submit?client_id={clientId}. If successful, the user details will be returned in the response. If not, validation errors will be returned.

Log in with Credentials

The login form should have fields for username and password. These should then be passed to AuthClient#loginWithCredentials. This method will then submit to {baseUrl}/embedded/login?client_id={clientId} with Content-Type: application/x-www-form-urlencoded.

The response will include a one-time-password to use in exchange for an access token. The AuthClient will automatically do so for you, submitting the token to the OAuth token endpoint similarly to non-embedded, but with some slight changes shown below.

POST '{baseUrl}/oauth/token?' \
  -H "Content-Type: application/x-www-form-urlencoded"
  -d accountId={accountId}
  -d client_id={clientId}
  -d code={token}
  -d grant_type=authorization_code
  -d purpose=OTP
  -d redirect_uri={redirectUri}
  -d scope={scope}
  -d username={form.username}

If successful, the access token will then be cached. If not, validation errors will be returned related to the login form.