How To Implement OIDC Authentication with React Context API and React Router
Fran Pastor
February 20, 2019

OpenId

Hi readers, I’m writing this story to avoid more people going crazy trying to implement oidc client with react, have authorized routes and use the class provided by the library to manage all the needed behaviour in our react applications.

To give you an example, this is a boilerplate of create-react-app with the implementation.

Franpastoragusti/oidc-react-app

Backend implementation is ready

First of all, this article is always going to talk about the frontend implementation. The identity server is currently implemented as this post of my partner Danny Garcia explain in this story that you should follow.

Creating your own Global Identity Provider - Part I

What we are going to apply?

We are going to implement the behavior of the diagram:

Diagram

  1. If a user tries to access a private route, the application is going to check if the user has been logged into the server, if authorized, we are going to allow the user access. If not we are going to redirect the user to the login screen to be logged and retry access to the private route.
  2. We are going to apply the mechanism needed to refresh the token of the user after an expiration time without the user realizing it.
  3. Finally, we are going to apply the behaviour needed in your register page to create users in our app.

Implementation

Install requirements

We are going to add to the project react-router-dom and oidc-client to our current project, in this case we are going to use a common create-react-app boilerplate to start

create-react-app oidc-react-app
yarn add react-router-dom oidc-client

Configuration files

First of all we are going to define the configuration needed to the oidc-client:

  • IDENTITY_CONFIG: The config needed to establish the connection.
  • METADATA_OIDC: The extra info that you want to have in the token.

/* /src/utils/authConst.js */
export const IDENTITY_CONFIG = {
authority: process.env.REACT_APP_AUTH_URL, //(string): The URL of the OIDC provider.
client_id: process.env.REACT_APP_IDENTITY_CLIENT_ID, //(string): Your client application's identifier as registered with the OIDC provider.
redirect_uri: process.env.REACT_APP_REDIRECT_URL, //The URI of your client application to receive a response from the OIDC provider.
login: process.env.REACT_APP_AUTH_URL + "/login",
automaticSilentRenew: false, //(boolean, default: false): Flag to indicate if there should be an automatic attempt to renew the access token prior to its expiration.
loadUserInfo: false, //(boolean, default: true): Flag to control if additional identity data is loaded from the user info endpoint in order to populate the user's profile.
silent_redirect_uri: process.env.REACT_APP_SILENT_REDIRECT_URL, //(string): The URL for the page containing the code handling the silent renew.
post_logout_redirect_uri: process.env.REACT_APP_LOGOFF_REDIRECT_URL, // (string): The OIDC post-logout redirect URI.
audience: "https://example.com", //is there a way to specific the audience when making the jwt
responseType: "id_token token", //(string, default: 'id_token'): The type of response desired from the OIDC provider.
grantType: "password",
scope: "openid example.api", //(string, default: 'openid'): The scope being requested from the OIDC provider.
webAuthResponseType: "id_token token"
};
export const METADATA_OIDC = {
issuer: "https://identityserver",
jwks_uri: process.env.REACT_APP_AUTH_URL + "/.well-known/openid-configuration/jwks",
authorization_endpoint: process.env.REACT_APP_AUTH_URL + "/connect/authorize",
token_endpoint: process.env.REACT_APP_AUTH_URL + "/connect/token",
userinfo_endpoint: process.env.REACT_APP_AUTH_URL + "/connect/userinfo",
end_session_endpoint: process.env.REACT_APP_AUTH_URL + "/connect/endsession",
check_session_iframe: process.env.REACT_APP_AUTH_URL + "/connect/checksession",
revocation_endpoint: process.env.REACT_APP_AUTH_URL + "/connect/revocation",
introspection_endpoint: process.env.REACT_APP_AUTH_URL + "/connect/introspect"
};
view raw authConst.js hosted with ❤ by GitHub

On the wiki you can check the explanation of a better description for each parameter.

IdentityModel/oidc-client-js

All the environment variables should be added to the .env files, these variables should be provided by the backend and match with the configuration of the server.

Create the authService

To manage the object provided by oidc we are going to create a class that is going to instance the object and is going to store the UserManager provided by the library. In this class we are going to create the methods to manage the user authentication and authorization flow.

import { IDENTITY_CONFIG, METADATA_OIDC } from "../utils/authConst";
import { UserManager, WebStorageStateStore, Log } from "oidc-client";
export default class AuthService {
UserManager;
constructor() {
this.UserManager = new UserManager({
...IDENTITY_CONFIG,
userStore: new WebStorageStateStore({ store: window.sessionStorage }),
metadata: {
...METADATA_OIDC
}
});
// Logger
Log.logger = console;
Log.level = Log.DEBUG;
this.UserManager.events.addUserLoaded((user) => {
if (window.location.href.indexOf("signin-oidc") !== -1) {
this.navigateToScreen();
}
});
this.UserManager.events.addSilentRenewError((e) => {
console.log("silent renew error", e.message);
});
this.UserManager.events.addAccessTokenExpired(() => {
console.log("token expired");
this.signinSilent();
});
}
signinRedirectCallback = () => {
this.UserManager.signinRedirectCallback().then(() => {
"";
});
};
getUser = async () => {
const user = await this.UserManager.getUser();
if (!user) {
return await this.UserManager.signinRedirectCallback();
}
return user;
};
parseJwt = (token) => {
const base64Url = token.split(".")[1];
const base64 = base64Url.replace("-", "+").replace("_", "/");
return JSON.parse(window.atob(base64));
};
signinRedirect = () => {
localStorage.setItem("redirectUri", window.location.pathname);
this.UserManager.signinRedirect({});
};
navigateToScreen = () => {
window.location.replace("/en/dashboard");
};
isAuthenticated = () => {
const oidcStorage = JSON.parse(sessionStorage.getItem(`oidc.user:${process.env.REACT_APP_AUTH_URL}:${process.env.REACT_APP_IDENTITY_CLIENT_ID}`))
return (!!oidcStorage && !!oidcStorage.access_token)
};
signinSilent = () => {
this.UserManager.signinSilent()
.then((user) => {
console.log("signed in", user);
})
.catch((err) => {
console.log(err);
});
};
signinSilentCallback = () => {
this.UserManager.signinSilentCallback();
};
createSigninRequest = () => {
return this.UserManager.createSigninRequest();
};
logout = () => {
this.UserManager.signoutRedirect({
id_token_hint: localStorage.getItem("id_token")
});
this.UserManager.clearStaleState();
};
signoutRedirectCallback = () => {
this.UserManager.signoutRedirectCallback().then(() => {
localStorage.clear();
window.location.replace(process.env.REACT_APP_PUBLIC_URL);
});
this.UserManager.clearStaleState();
};
}
view raw authService.js hosted with ❤ by GitHub

It’s important to think of the implementation in line 19 over the event raised by the library “addUserLoaded” that is going to control the flow and only in the case that the user comes from the login redirect url we are going to redirect the user to the principal private screen, allowing the event to renew the token with the silent renew mechanism.

Create UserContext

Now we have our class to manage the UserManager object. We are going to need one instance of this object in different places in our app, for example in the register page or in our private routes to validate if the user is allowed to access the route.

The library said it is prepared to be instanced multiple times and itself is going to check if another object is currently created, but for better organization of the code and to be sure to instance it only one time we are going to use the Context API of React.

So we are going to create the AuthContext, the AuthProvider with the instance of our AuthService and the AuthConsumer to be used in our application.

/* /src/providers/authProvider.jsx */
import React, {Component} from "react";
import AuthService from "../services/authService";
const AuthContext = React.createContext({
signinRedirectCallback: () => ({}),
logout: () => ({}),
signoutRedirectCallback: () => ({}),
isAuthenticated: () => ({}),
signinRedirect: () => ({}),
signinSilentCallback: () => ({}),
createSigninRequest: () => ({})
});
export const AuthConsumer = AuthContext.Consumer;
export class AuthProvider extends Component {
authService;
constructor(props) {
super(props);
this.authService = new AuthService();
}
render() {
return <AuthContext.Provider value={this.authService}>{this.props.children}</AuthContext.Provider>;
}
}

With that we achieve having our AuthContext the AuthService instanced, and if we need it we have it in the AuthConsumer.

Prepare the components to be used on routes

On this point we have the possibility to start to create consumers in our components, we are going to create the components first and define the routes after.

  • PrivateRoute: this component validates the user access, if the user is not authenticated they will be redirected to the login screen and after will come back to the requested route.

/* /src/routes/privateRoute.jsx */
import React from "react";
import { Route } from "react-router-dom";
import { AuthConsumer } from "../providers/authProvider";
export const PrivateRoute = ({ component, ...rest }) => {
const renderFn = (Component) => (props) => (
<AuthConsumer>
{({ isAuthenticated, signinRedirect }) => {
if (!!Component && isAuthenticated()) {
return <Component {...props} />;
} else {
signinRedirect();
return <span>loading</span>;
}
}}
</AuthConsumer>
);
return <Route {...rest} render={renderFn(component)} />;
};

Some components are needed to manage the user flow, these components execute different functions of AuthConsumer and they are going to be accessible by route.

  • callback: This component is going to be used after login when the user is going to be redirected by the server to the redirect_uri setted on the IDENTITY_CONFIG

/* /src/components/auth/callback.jsx */
import React from "react";
import { AuthConsumer } from "../../providers/authProvider";
export const Callback = () => (
<AuthConsumer>
{({ signinRedirectCallback }) => {
signinRedirectCallback();
return <span>loading</span>;
}}
</AuthConsumer>
);
view raw callback.jsx hosted with ❤ by GitHub

  • logout: This component is going to logout the user.

/* /src/components/auth/logout.jsx */
import React from "react";
import { AuthConsumer } from "../../providers/authProvider";
export const Logout = () => (
<AuthConsumer>
{({ logout }) => {
logout();
return <span>loading</span>;
}}
</AuthConsumer>
);
view raw logout.jsx hosted with ❤ by GitHub

  • logoutCallback: After logout the server is going to redirect the user to the post_logout_redirect_uri property of the IDENTITY_CONFIG where the logoutCallback is going to delete the cookie from the identity server and the localStorage cleaning the browser, and redirect the user to the REACT_APP_PUBLIC_URL defined in our .env file

/* /src/components/auth/logoutCallback.jsx */
import React from "react";
import { AuthConsumer } from "../../providers/authProvider";
export const LogoutCallback = () => (
<AuthConsumer>
{({ signoutRedirectCallback }) => {
signoutRedirectCallback();
return <span>loading</span>;
}}
</AuthConsumer>
);

  • silentRenew: Is going to be used by the event addAccessTokenExpired configured on the AuthService.

/* /src/components/auth/silentRenew.jsx */
import React from "react";
import { AuthConsumer } from "../../providers/authProvider";
export const SilentRenew = () => (
<AuthConsumer>
{({ signinSilentCallback }) => {
signinSilentCallback();
return <span>loading</span>;
}}
</AuthConsumer>
);
view raw silentRenew.jsx hosted with ❤ by GitHub

Create our router

Now that we have all the components needed we are going to set up our router. In our routes file we are going to create the routes and the components associated to each route.

import * as React from "react";
import { Route, Switch } from "react-router-dom";
import { Callback } from "../components/auth/callback";
import { Logout } from "../components/auth/logout";
import { LogoutCallback } from "../components/auth/logoutCallback";
import { PrivateRoute } from "./privateRoute";
import { Register } from "../components/auth/register";
import { SilentRenew } from "../components/auth/silentRenew";
import {PublicPage} from "../components/publicPage"
import {PrivatePage} from "../components/privatePage"
export const Routes = (
<Switch>
<Route exact={true} path="/signin-oidc" component={Callback} />
<Route exact={true} path="/logout" component={Logout} />
<Route exact={true} path="/logout/callback" component={LogoutCallback} />
<Route exact={true} path="/register" component={Register} />
<Route exact={true} path="/silentrenew" component={SilentRenew} />
<PrivateRoute path="/dashboard" component={PrivatePage} />
<Route path="/" component={PublicPage} />
</Switch>
);
view raw routes.jsx hosted with ❤ by GitHub

As you can see we are not creating a login screen or anything similar. In the backend the identity server side implements its own login and register screen by default, we are going to use these screens to register a new user and to be logged into the identity.

If you want to manage the register and the login you can implement both your react app, creating the route directly in the routes.js file and managing the forms inside the app.

Render all in our App

We have all the components and configurations done. We are going to use BrowserRouter with our routes config file, and we are going to pass it as children of our AuthProvider that is going to give access to the components to the consumer.

/* /src/app.jsx */
import React, {Component} from "react";
import { AuthProvider } from "./providers/authProvider";
import { BrowserRouter } from "react-router-dom";
import {Routes} from "./routes/routes";
export class App extends Component {
render() {
return (
<AuthProvider>
<BrowserRouter children={Routes} basename={"/"} />
</AuthProvider>
);
}
}
view raw app.jsx hosted with ❤ by GitHub

Everything should work

At this point if we have all the url’s working and our authConst configured all the mechanisms should work as specified in the diagram.

In this post, we saw how to implement oidc with react and react-router by an implementation of React Context API, I hope you enjoy the behaviour and if you have any questions regarding this or anything I should add, correct or remove, feel free to comment.

Thanks !!!

FrontEnd
React
Openid
React Router
JavaScript