Prowise React Auth
This package wraps the open-source oidc-client-ts
library and provides a
context, helpers and base set of config to enable authorizations for
front-end React applications.
Installation
Install with npm install @prowise/react-auth
You will need react@^18.2.0
as a dependency in your own project and you
will want to install deep-diff@^1.0.2
if you're not yet using it.
Implementation
All the public interfaces are re-exported from index.ts
, all other files
and functions are considered private.
Bootstrapping Config
During bootstrapping, create an auth manager object. The returned object is
the UserManager
from oidc-client-ts
. When using server side rendering (SSR) you can do this in
componentDidMount
or equivalent.
The settings
object allows you to override config passed to the UserManager
and allows you to set a home_path
and error_path
which will be arguments
passed to the redirector component in certain scenarios.
import { createAuthManager } from "@prowise/react-auth";
// ...
createAuthManager(config.authority, config.clientId, settings);
// ...
See also the example implementation for this.
Setup Context Provider
Wrap your application/routes with the context provider setup / managed by the package. The following props are optional:
-
signinSilent
will immediately try to assess login status on the background. -
removeStaleState
will clean up stale state requests after the user has successfully authorized. -
removeExpired
will remove an expired user from the store -
managedSigninCallback
will manage the logic for handling the auth callback for you, preventing the rendering of children of the Provider while the user is in the callback. NOTE: you do not need to use the AuthCallback component if this is true (default value) -
loader
the loader component to render when the user is on the authCallback page. This is only required if managedSigninCallback is kept on its default (true)
import React, { FC } from "react";
import { AuthProvider } from "@prowise/react-auth";
import MyAppRoutes from "routes";
const Loader: FC = () => <div>Processing auth callback...</div>
const MyApp: FC = () => (
<AuthProvider loader={Loader} signinSilent removeStaleState removeExpired>
<MyAppRoutes />
</AuthProvider>
);
export default MyApp;
For the most simple example, see the example implementation.
Note that it is common practice to add the protected routes in a <Route>
tree as followed:
<BrowserRouter>
<Routes>
<Route path="/auth/error" element={<>There was an authentication error</>} />
<Route element={<ProtectedRoutes />}>
<Route
path="/"
element={
<>Protected content</>
}
/>
</Route>
</Routes>
</BrowserRouter>
Where <ProtectedRoutes />
typically looks like:
let isLoggingIn = false;
/**
* Exposes mostly the same interface as the normal Route. Ensures a user is
* logged in before rendering the given component.
*
* @returns Component.
*/
export const ProtectedRoutes: FC = () => {
const auth = useAuth();
return useMemo(() => {
if (!auth.isAuthenticated && !auth.isChecking) {
// Not logged in.
if (!isLoggingIn) {
isLoggingIn = true;
void auth.login();
}
}
if (!auth.isAuthenticated) {
return <>Redirecting to login</>;
}
// Logged in.
isLoggingIn = false;
return <Outlet />;
}, [auth]);
};
Setup Callback Route
Note: This setup is only needed if you want to implement your own custom callback handling / routing Per default the AuthProvider does this for you, but you can set AuthProvider prop managedSigninCallback=false to manage it yourself. If you do choose to implement this, please make sure to set the implementation as early as possible in your project to prevent side effects during silent signin.
Implement the callback component on the callback URL, by default on the path
/auth/callback
but this can be overridden with redirect_uri
in settings
.
The redirector will get passed the path the user was on when the interactive login was triggered. The loader is rendered while the process is still being completed. Both parameters are required in this case.
The callback should be an unprotected route.
import React, { FC } from "react";
import { Redirect } from "react-router-dom";
import { AuthCallback } from "@prowise/react-auth";
const MyCallback: FC = () => (
<AuthCallback
redirector={({ to }) => (<Redirect to={to} />)}
loader={() => <>Checking session status...</>}
/>
);
export default MyCallback;
Route implementation:
const MyApp = () => (
<AuthProvider signinSilent={true} managedSigninCallback={false} >
<Router>
<Switch>
<Route path="/" exact element={<Home />} />
<MyProtectedRoute path="/protected" element={<User />} />
<Route path="/about" element={<About />} />
<Route path="/auth/callback" element={<MyCallback />} />
<Route path="/auth/error" element={<Error />} />
</Switch>
</Router>
<MyStatus />
</AuthProvider>
);
Do NOT do this when implementing the Route (because the logic inside those wrappers will be run when the user is on the auth callback route):
const MyApp = () => (
<AuthProvider signinSilent={true} managedSigninCallback={false} >
<WrapperWithSideEffects>
<WrapperWithSomeApiCalls>
<AnotherWrapper>
<Router>
<Switch>
<Route path="/" exact element={<Home />} />
<MyProtectedRoute path="/protected" element={<User />} />
<Route path="/about" element={<About />} />
<Route path="/auth/callback" element={<MyCallback />} />
<Route path="/auth/error" element={<Error />} />
</Switch>
</Router>
</AnotherWrapper>
</WrapperWithSomeApiCalls>
</WrapperWithSideEffects>
<MyStatus />
</AuthProvider>
);
See also the example implementation for this.
Setup Silent Signin Callback route to improve performance
Per default, the auth library will handle the silent signin callback in the AuthCallback component.
This will render the /auth/callback
route in an iframe in your app.
The downside of this is that it will request the full app resources in the iframe that handles the silent auth callback, resulting in possible performance issues.
To prevent this, you can implement a custom silent signin callback page, with minimal javascript to handle the callback.
Create the following files in your public directory (ideally under a subfolder auth
so your URL is alike the other auth
URLs).
silent.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Prowise Silent Signin</title>
</head>
<body>
<script type="module" src="./silent.js"></script>
</body>
</html>
silent.js
/**
* Logic is cloned from oidc-client-ts source code
* https://github.com/authts/oidc-client-ts/blob/main/src/navigators/AbstractChildWindow.ts
*
* messageSource might change as this is a hardcoded variable in oidc-client-ts source code
*/
const messageSource = "oidc-client"; // As set by oidc-client-ts
const url = window.location.href;
const targetOrigin = window.location.origin;
try {
// oidc-client-ts expects and handles the postMessage
window.parent.postMessage({
source: messageSource,
url,
keepOpen: false,
}, targetOrigin);
} catch (e) {
console.error("Silent signin failed", e);
}
Make sure to add the file path to your AuthManager settings.
createAuthManager(ssoAuthority, ssoClientId, {
silent_redirect_uri: `${window.location.origin.toString()}/auth/silent.html`,
});
This new URL then needs to be added to our SSO database (OAUTH_CLIENT_URL
) as accepted URL.
Use hook to get status
Once the above two are implemented you can use the useAuth
hook to get the login
state of a user, given the client used is authorized.
import React from "react";
import {useAuth} from "@prowise/react-auth";
const MyStatus = () => {
const auth = useAuth();
return (
<>
<pre>{JSON.stringify(auth, null, 2)}</pre>
{auth.isAuthenticated ?
<button onClick={auth.logout}>Logout</button> :
<button onClick={auth.login}>Login</button>
}
</>
);
};
export default MyStatus;
See also the example implementation for this.
GetAccessToken to always make sure you use an up to date token in your request
Prowise tokens expire in an hour. To prevent unneeded calls when you have an expired token,
you can now use the getAccessToken which is available on the returned manager when using createAuthManager
.
Simply wait with doing the API call until you get a response from this Promise. You can then use the resulting token in your request header.
import { createAuthManager } from "@prowise/react-auth";
// ...
const auth = createAuthManager(config.authority, config.clientId, settings);
// ...
// Then you can use this wherever your logic resides to attach a token to each request
const token = await auth.getAccessToken();
// Process the token & do your api call here
Login_hint Query Parameter
The login_hint query parameter can be passed on to every URL of your application and will be automatically picked up and removed from the URL. It will then be passed on every sign-in request to the SSO, and stored until you close the browser tab/window.
Examples
Further examples can be found in the examples folder - if you want to run it you will have to have run the following (in the root directory):
nvm use
npm i
Run the examples with:
npm run example
Notes
Expiration notification
To speed up the expiration notification mechanism, enable the property accessTokenExpiringNotificationTimeInSeconds
within src/utilities/CreateAuthManager.ts
.
Firefox redirect loop
If your browser is stuck in a redirect loop (most likely Firefox), it may be configured to deny third party cookies. To fix:
- Go to Settings > Privacy & Security
- Choose Custom
- Uncheck Cookies