yarn add @broadleaf/auth-react
The Broadleaf Auth React SDK is designed to make interacting with the Broadleaf Authorization Server easier for a single-page React application.
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;
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;
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')
);
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;
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;
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
);
}
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).
|
(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.
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
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.