Broadleaf Microservices
  • v1.0.0-latest-prod

Auth Web SDK

Design Details

This SDK by default provides security for SPAs with Authorization Code Flow and silent authentication using an iframe. It is also possible to enable the Proof Key for Code Exchange (PKCE) enhancement using the usePkce client option when instantiating the AuthClient.

Important
PKCE also needs to be enabled in your backend auth server—see the Authorization docs for details.

In addition, it can be configured to instead use a Refresh Token Rotation (RTR) to handle recent changes in browser privacy standards that block third-party cookies such as Intelligent Tracking Protection. To enable RTR, use the useRefreshTokens client option when instantiating the AuthClient. This will also automatically include the OFFLINE_ACCESS scope in any requests.

Important
RTR also needs to be enabled in your backend auth server—see the Authorization docs for details.

Token Storage

By default tokens will be stored in browser memory. However, another type of cache may be provided at time of client instantiation. It must implement the TokenCache interface.

When using RTR, the SDK falls back onto using silent authorization with an iframe in the event the user refreshes the browser since the tokens are store in-memory. Otherwise, a login_required error will be thrown, and can be handled by redirecting the user to login. Falling back on an iframe can be disabled using the useIFrameAsFallbackForRefreshToken client option when instantiating the AuthClient.

Local or Session Browser Storage (as of 1.5.3)

One alternative introduced when using Refresh-Token-Rotation is to store the token in the browser session or local storage. This is to support retrieving an access token when a session cookie (BLSID) is not available, usually only for cross-origin auth situations where the Auth server and frontend apps are on different domains. According to Auth0, it is acceptable to use browser storage with refresh token rotation as long as re-use detection is implemented, which Broadleaf has: https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/#You-Can-Store-Refresh-Token-In-Local-Storage.

This feature is only allowed when using refresh token rotation and not when using silent-auth with iframe.

To enable, pass useRefreshTokens and useSessionStorageForTokenCache or useLocalStorageForTokenCache as true to AuthProvider or AuthClient. Also ensure that useIFrameAsFallbackForRefreshToken is false.

Example Configuration
<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
>

Installation

This library can be installed using Yarn or NPM:

Yarn

yarn add @broadleaf/auth-web

NPM

npm install @broadleaf/auth-web

Getting Started

This section will take you through how to get started with using the @broadleaf/auth-web library in your single-page application.

Create a Client

The first step is to create a new AuthClient that we can use to execute operations against the authorization server.

import { AuthClient } from '@broadleaf/auth-web';

const auth = new AuthClient({
  baseURL: '/auth',
  clientId: 'my-app'
});

Login

In order to login a user, we need to use loginWithRedirect to initiate the login flow:

import { AuthClient } from '@broadleaf/auth-web';

const auth = new AuthClient({
  baseURL: '/auth',
  clientId: 'my-app',
  redirectUri: window.location.origin
});

// this will redirect the user to the authorization server to log in
auth.loginWithRedirect();

Upon successfully logging in, the user will be redirected back to the application redirectUri provided to the client with additional query parameters. This redirection should then be handled by the handleRedirectCallback function:

import { AuthClient } from '@broadleaf/auth-web';

const auth = new AuthClient({
  baseURL: '/auth',
  clientId: 'my-app',
  redirectUri: window.location.origin
});

(async () => {
  await auth.handleRedirectCallback();

  // user must now be logged in
})();

Securing API Calls

Once we are logged in, we can now use getAccessToken to retrieve an access token to secure out HTTP calls to our APIs:

import { AuthClient } from '@broadleaf/auth-web';

const auth = new AuthClient({
  baseURL: '/auth',
  clientId: 'my-app',
  redirectUri: window.location.origin
});

(async () => {
  if (auth.isAuthenticated()) {
    try {
      const accessToken = await auth.getAccessToken();

      const response = await fetch('/resolve-cart', {
        headers: {
          'Authorization': `Bearer ${accessToken}`
        }
      });
      const cart = await response.json();

      // do something with the cart
    } catch (e) {
      console.error('Something went wrong', e);
    }
  }
})();
Note

In order to support the Silent Authentication Flow, you must set up your app server to serve the silent callback file.

Logout

Lastly, we will want to provide the user a way to logout, which is where we will use logoutWithRedirect:

import { AuthClient } from '@broadleaf/auth-web';

const auth = new AuthClient({
  baseURL: '/auth',
  clientId: 'my-app',
  redirectUri: window.location.origin
});

auth.logoutWithRedirect({
  returnTo: window.location.href
});

Usage

Login with redirect

Initiates an OAuth2 Authorization Code Grant with a redirect to login. Upon successfully logging in, this will redirect the user back to the application at the redirect URI with query parameters. The query parameters will look like ?code=<code>&state=<state> if successful, and ?error=<error>&error_description=<error_description> if not.

This should be followed up with a handleRedirectCallback at the application entrypoint to process the granted code or error parameters and ensure the access and refresh tokens (if using Refresh Token Rotation) are cached.

auth.loginWithRedirect();

You can also provide a different redirectUri as long as it is added as a valid callback URL in the authorization server:

auth.loginWithRedirect({
  redirectUri: `${window.location.origin}/login-success`
})

Additionally, you can provide application state that will be stored in session storage and retrieved upon successfully handling the callback:

auth.loginWithRedirect({
  appState: {
    cartId: '123',
    from: '/cart'
  }
});

Handle redirect callback

Handles processing the parameters provided as part of the redirect callback initiated by loginWithRedirect. This method parses the request parameters, requests the access token, and updates the in-memory token cache. This is necessary to fully initiate an authenticated user in the single-page application:

const { appState } = await auth.handleRedirectCallback();

// user is authenticated

Handling errors is important to ensure we handle situations where the redirect callback yields an error:

try {
  await auth.handleRedirectCallback();
} catch (e) {
  // handle the error
}

Logout with redirect

Initiates a logout for the current user and redirects the user upon success:

auth.logoutWithRedirect({
  returnTo: window.location.href
});

Get Access Token

Initiates a flow to retrieve an access token for the user. By default, this uses a hidden iframe to initiate an OAuth2 Authorization Code Grant to retrieve an access token without requiring a redirection for the user.

await auth.getAccessToken();

A scope can be provided as an option to retrieve an access token for a custom scope:

await auth.getAccessToken({ scope: 'CUSTOM_SCOPE' });

Using Proof-Key-for-Code-Exchange (PKCE, since 1.6.0)

To use the Proof-Key-for-Code-Exchange enhancement for the authorization code grant flow , pass usePkce into the AuthClient when instantiating it.

const auth = new AuthClient({
  usePkce: true
});

Using Refresh Token Rotation

To use Refresh Token Rotation instead of the auth code grant flow with silent authorization, pass useRefreshTokens into the AuthClient when instantiating it.

const auth = new AuthClient({
  useRefreshTokens: true
});

To also disable falling back on the hidden iframe if the refresh token is missing (such as when the user has refreshed the page), pass in useIFrameAsFallbackForRefreshToken as well.

const auth = new AuthClient({
  useRefreshTokens: true,
  useIFrameAsFallbackForRefreshToken: false
});

Supporting Silent Authentication Flow

By default, getAccessToken uses a hidden iframe to execute the OAuth2 Authorization Code Grant with or without 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 initializing the client:

import { AuthClient } from '@broadleaf/auth-web';

const auth = new AuthClient({
  baseURL: '/auth',
  clientId: 'my-app',
  silentRedirectUri: `${window.location.origin}/my-silent-callback.html`
});

Check Session

Attempts to retrieve an access token in order to populate the token cache and detect if the user is authenticated. This is typically used after initializing the AuthClient to establish the local authentication state when not handling the login redirect with handleRedirectCallback.

const auth = new AuthClient({
  baseURL: '/auth',
  clientId: 'my-app'
});

await auth.checkSession();

if (auth.isAuthenticated()) {
  // user is authenticated
} else {
  // user is not authenticated
}

This can also be used to implement keep-alive behavior for a user’s session by calling this on an interval to continually update the user’s token cache:

setInterval(async () => {
  await auth.checkSession();

  if (auth.isAuthenticated()) {
    // user is still authenticated
  } else {
    // user is no longer authenticated
  }
}, 1000 * 60 * 5);

Is Authenticated?

Returns whether it knows the user is authenticated by checking if a token exists for the default user scope within the in-memory token cache.

if (auth.isAuthenticated()) {
  // user is authenticated
} else {
  // user is not authenticated
}

Get Identity Claims

Retrieves the identity claims from the cached access token. This can be useful for extracting claims and user information within the application.

const { email_address, full_name } = auth.getIdentityClaims();

// use email or name

Get Session Expiration

Retrieves the latest session expiration details from the cached access token. This is useful for situations when you want to implement an inactivity timer and need to understand how much time is left in a user’s session:

const {
  // when the session expires in ISO8601 string format
  inactivityAt,
  // when the user must login again in ISO8601 string format, the inactivity time can be refreshed up until this date
  requiresLoginAt
} = auth.getSessionExpiry();

Change Password

Initiates the change password flow for the current user by redirecting them to the authorization server. Upon success, this flow will redirect the user back to the application with the either the provided or default redirect URL.

auth.changePasswordWithRedirect({
  returnTo: window.location.href
});

Get User Info

Executes a request to retrieve the user information explicitly from the authorization server. This should only be used in special cases, as the getIdentityClaims method is a more efficient way of retrieving user information and does not require an additional HTTP request.

const user = await auth.getUserInfo();

Register with Redirect

Redirects the user to register on the authorization server. Upon a successful registration, the user will be redirect back to the application using the provided returnTo URL.

Note
The returnTo location must be a valid redirect URI for the authorized client. This can be configured in the admin UI by . going to Security → Authorization Servers → Select a server → Authorized Clients → Select the client

Handling Embedded Forms

The previous sections covered the default, recommended methods for performing authentication operations. They are inline with Universal Login, which is more secure and easier to implement than embedded login. However, embedded login is also supported with a little configuration.

Embedded login is where the authentication forms (login, registration, change password, reset password) are hosted outside the Authentication Microservice such as embedded in the commerce app. This allows the user to remain in the current app context rather than being redirected to an auth server. This is inherently less secure but may be seen as a requirement for user-experience.

Important
Embedded login is only supported iff the client app (e.g. commercweb) is hosted behind the same gateway as the Authentication Service, i.e., using the default configuration from Broadleaf. We do not support embedded login with cross-origin requests.

Enabling Embedded Form Support

To enable this, you must set broadleaf.auth.login.embedded.enabled to true in the Authentication Microservice’s configuration files. Then, the individual AuthorizationServer to which the client belongs must also have embeddedLoginEnabled set to true.

Available Methods

The AuthClient comes with the following methods for handling embedded auth operations, similarly named for convenience.

Login with Credentials

Takes a username and password and submits it to the auth service. If successful, the request will include the session cookie. Following a successful request, the implementor must make a separate request to fetch an access token.

If unsuccessful, the response will be a 401.

There will be no content in the response.

try {
  await auth.loginWithCredentials({ username: 'example@email.com', password: 'pass' });
  // follow auth code grant flow to get access token
} catch (err) {
  // show user message that their credentials were rejected
}

Register with Credentials

Takes the registration form values and submits them to the auth service. A successful registration attempt will include the User details in the response. If enabled on the server (via broadleaf.auth.controller.auto-login-after-registration), the user may also be automatically signed-in, and a session cookie will be included in the response.

If unsuccessful due to form errors, an ApiValidationError response will be returned mapping the rejected fields to a message.

try {
  const { data: user } = await auth.registerWithCredentials({
    username: 'example@email.com',
    password: 'pass',
    passwordConfirmation: 'pass',
    fullName: 'First Last',
    email: 'example@email.com',
  });
  // follow auth code grant flow to get access token
} catch (err) {
  if (err.response?.data) {
    // show user the validation errors: err.response.data.fieldErrors['field'].reason
  }
}

Change Password with Credentials

Takes the old and new password values and submits them to the auth service. A success will return a 204 response. A failure due to invalid values will be returned mapping the rejected field to an error message.

try {
  await auth.changePasswordWithCredentials({
    currentPassword: 'pass',
    newPassword: 'newPass',
    newPasswordConfirm: 'newPass'
  });
} catch (err) {
  if (err.response?.data) {
    // show user the validation errors: err.response.data.fieldErrors['field'].reason
  }
}

Takes the a username submits them to the auth service. A success will return a 204 response, and an email will be sent to the user with a link to follow to reset their password. The link will include a one-time use token required to successfully process their new password submission. A failure due to invalid values will be returned mapping the rejected field to an error message.

try {
  await auth.requestResetPasswordLinkWithCredentials({
    username: 'example@email.com'
  });
} catch (err) {
  if (err.response?.data) {
    // show user the validation errors: err.response.data.fieldErrors['field'].reason
  }
}

Reset Password with Credentials

Takes the a username, new password, and the token included in the reset-password link sent to the user’s email. A success will return a 204 response, and the user will be auto-logged if the auth service is configured to do so (via broadleaf.auth.controller.auto-login-after-password-reset). The link will include a one-time use token required to successfully process their new password submission. A failure due to invalid values will be returned mapping the rejected field to an error message.

try {
  await auth.changePasswordWithCredentials({
    username: 'example@email.com',
    newPassword: 'newPass',
    token: 'ABC_123'
  });
} catch (err) {
  if (err.response?.data) {
    // show user the validation errors: err.response.data.fieldErrors['field'].reason
  }
}