OAuth2 PKCE flow is an adjustment of OAuth2 authorization_code for Single Page Applications (S.P.A. - i.e. the javascript application) or mobile application. It makes the flow more adapted to a client "orchestrator" and enables and more secured for this case.

PKCE reminder

A quick reminder is that PKCE flow is, as authorization_code flow, mainly a dance with the OAuth2 server:

  1. the SPA redirects the user on the OAuth2 server with these parameters:

    • response_type=code: when coming back on the application we’ll want a code to be able to get a token,

    • client_id: the client which represents the configuration used to generate the token for the application in general,

    • redirect_uri: where to redirect the user to once the flow is finished on server side,

    • state: an optional value which lets the client application get back some data once the flow is finished (can be used to store the page the user was trying to go on before being redirected to be logged in) - generally it is just an identifier of data stored in the client (local storage or other), not the data themselves.

    • code_challenge: a generated code which will guarantee the application triggering the flow and the one requesting the token is the same, it is generally a cryptographic like string (relatively random and secure) which is then hashed with a SHA-256 and base64 url encoded,

    • code_challenge_method: generally S256 to say the challenge is a hash, it can be in plain text too but it is not recommended so in practise it is always S256.

  2. the user will end up on the login form (of the OAuth2 server generally - what is important is it is not the SPA until you add some setup to enable it),

  3. once the user logged in the OAuth2 server redirects the user on the SPA (thanks the redirect_uri) with the following parameters:

    • state: the initial state (to let the SPA continue its business if needed),

    • code: a code enabling to get a token,

  4. the SPA can call the token endpoint to get a token with these parameters:

    • grant_type=authorization_code: for PKCE the grant_type is authorization_code too,

    • code: the obtained code after the previous redirection,

    • redirect_uri: still the same redirect_uri - which kind of makes part of the client identify for this authentication,

    • client_id: still the same client id,

    • code_verifier: the original code which enabled to generate the code_challenge.

At that stage, if everything went well, the SPA has a token - and potentially a JWT if the OAuth2 server uses this kind of token.

React.JS and PKCE

Create a react application

To illustrate how easy it can be to integrate with a PKCE OAuth2 server from a React.JS application let’s generate a react sample application. If you never did, install create-react-app package (optionally globally for your user with -g option):

npm add -g create-react-app

Once you have create-react-app helper you can generate a React.JS application:

npx create-react-app my-app

Now you can go in my-app folder and run npm start to see what the application looks like. You should see something like:

default react app

As mentionned by the application, you can write/customize the application editing src/App.js. For this post, we don’t care much of what the application is/does and we will just secure it.

Install react-oauth2-pkce

To secure the application, we will start by installing react-oauth2-pkce:

npm install --save react-oauth2-pkce

Wrap App in SecuredApp component

Then, we will create a src/SecuredApp.js file which will wrap our App component to add the security. To make it iterative we will just start by making it 1-1 with App:

src/SecuredApp.js
import React from 'react';
import App from './App';

function SecuredApp() {
    return (
        <App />
    );
}

export default SecuredApp;

Then, we replace App by SecuredApp as root component in src/index.js:

src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import SecuredApp from './SecuredApp'; (1)
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    <SecuredApp /> (2)
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
1 We import our secured component instead of the original App one,
2 We replace the original App component by the secured one as rendered root component (let’s ignore StrictMode one).

Enable PKCE flow

The PKCE react library we installed is a context so we need to wrap the application in the context provider. The context provider takes an authService as parameter which basically configure the flow (the initial parameter we saw in the first part liek client_id, code_challenge etc…​).

The first step is to import this library, this can be done with this line in our SecuredApp.js file:

import {
  AuthProvider, (1)
  AuthService, (2)
  useAuth, (3)
} from 'react-oauth2-pkce'
1 The provider is what will wrap the application and enable to use the associated context in our nested components,
2 The service is the flow configuration for that particular application,
3 useAuth enables to lookup the context (we’ll see it soon).

Since the context provider needs an authentication service, let’s create it at the top of our file:

const authService = new AuthService({ (1)
    clientId: 'my-app-client', (2)
    provider: 'http://localhost:8080/oauth2', (3)
    redirectUri: 'http://localhost:3000', (4)
    scopes: ['openid'], (5)
});
1 We create an AuthService thanks the import we just saw,
2 We set the needed client for the flow,
3 We set the base url for the flow (will detail it just after),
4 We set the redirect URI configured in the client, it must be an URL of the SPA,
5 We optionally set the request scopes (not always needed by the OAuth2 server, in particular with JWT).
point 3 works based on convention without more configuration/code, this means the authentication form will be http://localhost:8080/oauth2/authorize and the token endpoint will be http://localhost:8080/oauth2/token.

Now we have an authService we can create a component wrapping our SecuredApp:

src/SecuredApp.js
// SecuredApp as before

function WrappedSecuredApp() {
    return (
        <AuthProvider authService={authService} > (1)
            <SecuredApp />
        </AuthProvider>
    );
}

export default WrappedSecuredApp; (2)
1 We wrap SecuredApp (the function we saw earlier) in an AuthProvider,
2 Instead of exporting directly our SecuredApp we export the context aware component: WrappedSecuredApp.

Now, the index.js file will import WrappedSecuredApp as SecuredApp so no change is needed thanks to default export usage.

indeed, you can put WrappedSecuredApp in another file but it is not required.

Finally, we need to actually implement our SecuredApp component but since it is now wrapped in an AuthProvider, we can use useAuth to get the authentication state and optionally the token:

function SecuredApp() {
    const { authService } = useAuth(); (1)

    const login = async () => authService.authorize(); (2)
    const logout = async () => authService.logout(); (2)

    if (authService.isPending()) { (3)
        return <div>
            Loading...
            <button onClick={() => { logout(); login(); }}>Reset</button>
        </div>
    }

    if (!authService.isAuthenticated()) { (4)
        return (
            <div>
                <p>Not Logged in yet</p>
                <button onClick={login}>Login</button>
            </div>
        )
    }

    const token = authService.getAuthTokens(); (5)
    return ( (6)
        <div>
            <button onClick={logout}>Logout</button>
            <App />
        </div>
    );
}
1 Thanks to useAuth we can lookup the authService, in this particular case it is not critical since we have it at the top of the file but when using in nested components (wrapping them with withAuth from the same package) it is very useful,
2 We create login and logout callback just delegating to authService - in async mode - to initiate the authentication flow or reset the stored data respectively,
3 If the authentication is pending (i.e. flow is in progress) we show some loading feedback component (meterialized here by a simple "Loading" text), note that it can be neat to enable the user to stop the in progress flow (if something goes wrong in background) or ensure there is a timeout on this state (if not set, start it using setTimeout for example),
4 If the user is not authenticated - and flow is not pending here, show the anonymous view (materialized by the login button here),
5 If we reach this stage, we have a token (so we are authenticated), we can access the token with authService.getAuthTokens() and condition the rendering with it if it is a JWT,
6 Finally we render the authenticated view (materialized by the logout button and the original App here).
in practise, nested component of App - like the menu bar - will use withAuth to have access to the authService too and be able to access the token.

Use the JWT to conditionally render part of the component

As soon as you use a JWT as token you can read it to conditionally render part of your screen. Here is how to do it:

import jwtDecode from 'jwt-decode' (2)
import { useAuth } from 'react-oauth2-pkce'

function SecuredNestedComponent() {
    const { authService } = useAuth(); (1)
    const jwtPayload = jwtDecode(authService.getAuthTokens().access_token); (2)
    const roles = jwtPayload.groups || []; (3)
    return (
        <div>
            <button onClick={logout}>Logout</button>
            {roles.includes('admin') && <AdminView />} (4)
            <UserView /> (5)
        </div>
    );
}

export default SecuredNestedComponent;
1 We retrieve our authService as "usual",
2 We use the jwt-decode library - imported by react-oauth2-pkce so it is already installed - to decode our access_token,
3 Assuming we have in the JWT the application roles in groups we extract this string array to use it in the rendering,
4 If the user has the role admin we render the admin view part of the screen,
5 In all cases we render the user view of the screen.

This simple example shows how a JWT makes it very easy and efficient - note that no remote call was needed at all to get the roles!

Conclusion

In this post we saw how secure a React.JS application with react-oauth2-pkce and how a JWT usage can be the backend scalable keeping the flexibility needed by the front on a very simple example.

Some important things to keep in mind about this topic are:

  • All javascript libraries use conventions and not all will work with all OAuth2 server without implementing some customizations (here if you don’t use /authorize and /token as endpoint after the provider value of the authService the library will not work for example),

  • The OAuth2 server must support CORS to be set up like that or be hidden behind the same HTTP server/Gateway than the SPA,

  • The authentication form is generally the OAuth2 server one but using a Gateway to hide it behind the same host than the SPA, you can make it hosted in the SPA generally (requires some setup but enables to have an uniform UI),

  • Using a JWT enables to have user context without any remote call enabling a better scalability,

  • Using a JWT also requires a good setup of the expiry of the token (generally "fast" - some minutes) to keep it secure,

  • The example was assuming the OAuth2 server was running locally (localhost:8080) and over HTTP but in practise it must be secured (HTTPS).

So overall, even if technically there is nothing crazy in using an OAuth2 server, it still requires a good understanding of the overall system to properly tune the client configuration and not only focus on the javascript code ;).

From the same author:

In the same category: