Check out the free virtual workshops on how to take your SaaS app to the next level in the enterprise-ready identity journey!

Simple User Authentication in React

Simple User Authentication in React

In 2019, it’s quite easy to find React components for pretty much everything. For example, if you want to add user authentication to your app, you can do so easily with Okta’s React component. Here I’ll walk you through creating a simple, fun React app that fetches random Chuck Norris jokes. I’ll then show you how you can add user authentication and customize your user experience, so the jokes will replace Chuck Norris’ name with their own.

Bootstrap Your React App

To get React up and running quickly without a lot of hassle, you can use React’s create-react-app script. React also prefers yarn if you have it installed. You can install those both with this command:

npm install --global create-react-app@2.1.5 yarn@1.12.3

Once you have those installed, you can create a new app with the following:

create-react-app chuck-norris-jokes

The script will create a new directory with some starter files, install a slew of dependencies needed to get things up and running and initialize the project with git. You can now change into the directory and start the development server, which will open the app in your web browser. Your browser will automatically update whenever you change any source files.

cd chuck-norris-jokes
yarn start

React start app

Add Some Style to Your React App

First things first, you’ll need to change out the default logo. Since this will be showing Chuck Norris jokes, find your favorite image of Chuck Norris (or just use the one below).

Chuck Norris bckground

Save the file to src/chuck-norris.png. Then open up src/App.js and change the line import logo from './logo.svg' to import logo from './chuck-norris.png'.

If you used the image above, the default dark background will make it hard to see. You also don’t really need his head to be spinning around, so open up src/App.css and make a few changes. You can get rid of the animation line (and you can get rid of the @keyframes block. You’ll also want to change the background-color to something like gray. You’ll want to add a button, but you can style it to look just like the Learn React link that’s already there. In the end, your CSS should look something like this (I also added a little padding to the header for small screens):

.App {
  text-align: center;
}

.App-logo {
  height: 40vmin;
  pointer-events: none;
}

.App-header {
  background-color: gray;
  padding: 10px;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

button.App-link {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}

Use React Hooks to Fetch Some Jokes

With the release of React 16.8, you can now use hooks to make your code components a little simpler. It’s not totally necessary, but, why not? In your src/App.js, change the App component from a class to a function. The default looks something like this:

class App extends Component {
  render() {
    return (
      // ...
    );
  }
}

To make it a functional component, change it to look more like this:

const App = () => {
  return (
    // ...
  )
}

It’s not a huge difference, but it’ll allow us to add hooks in, which are a bit simpler than using the class lifecycle methods.

To add a piece of state with hooks, you’ll need to use the useState function exported from React. You’ll also be using useEffect later on, so you’ll need to make sure to import both of those. Change the line importing React to look like this:

Import React, { useState, useEffect } from 'react';

Then to use a piece of state, add the following to the top of your App function, before the return statement:

const [joke, setJoke] = useState('');

You now have a way to read a joke and change its value. By default, it will just be an empty string until you fetch it. Speaking of fetching, you can use The Internet Chuck Norris Database to fetch jokes. They come already encoded for HTML, but React expects strings to be decoded (e.g. " instead of "). You can install the he library to handle this for you, then import it using import { decode } from 'he'. Install it using:

yarn add he@1.2.0

Now you’ll need to write a function to fetch jokes. Since this will need to reference setJoke, it should be written inside the App component. The fetch API supports a signal that will allow you to cancel the call. This would be important to make sure calls come back in the right order. If you have two or three calls successively, you don’t really care about the first two, but instead, just want the final result.

const fetchJoke = async signal => {
  const url = new URL('https://api.icndb.com/jokes/random');
  const response = await fetch(url, { signal });
  const { value } = await response.json();

  setJoke(decode(value.joke));
};

You’ll also need an “effect” hook to fetch the joke whenever there isn’t one. The useEffect hook will run any time the component renders. However, you can add an array of values to watch, which will cause it to only run the effect when one of those values changes. In this case, you’ll only want to run the effect when the joke changes, or fetch one if there is no joke set yet. If the effect has a return value, it will be run when cleaning up the app, or before rendering again. Here is where you can provide a function that will cancel the fetch call.

useEffect(() => {
  if (!joke) {
    const controller = new AbortController();
    fetchJoke(controller.signal);

    return () => controller.abort();
  }
}, [joke]);

Now you just need to display the joke and add a button to fetch a new one. Clicking the button will set the joke back to an empty string, and the effect will take care of fetching a new joke. Replace the contents of the <p> tag with the joke (<p>{joke || '...'}</p>), and replace the <a> tag with a button:

<button className="App-link" onClick={() => setJoke('')}>
  Get a new joke
</button>

Your final code for App.js should look like this:

import React, { useState, useEffect } from 'react';
import { decode } from 'he';

import logo from './chuck-norris.png';
import './App.css';

const App = () => {
  const [joke, setJoke] = useState('');

  const fetchJoke = async signal => {
    const url = new URL('https://api.icndb.com/jokes/random');
    const response = await fetch(url, { signal });
    const { value } = await response.json();

    setJoke(decode(value.joke));
  };

  useEffect(() => {
    if (!joke) {
      const controller = new AbortController();
      fetchJoke(controller.signal);

      return () => controller.abort();
    }
  }, [joke]);

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>{joke || '...'}</p>
        <button className="App-link" onClick={() => setJoke('')}>
          Get a new joke
        </button>
      </header>
    </div>
  );
}

export default App;

Jokes app running

Add User Authentication to Your React App

Now that you have a functional web app, you can add some personalization by allowing users to sign in. One simple way to add authentication to your project is with Okta, which is what I’ll demonstrate. Our API enables you to:

Before you begin, you’ll need a free Okta developer account. Install the Okta CLI and run okta register to sign up for a new account. If you already have an account, run okta login. Then, run okta apps create. Select the default app name, or change it as you see fit. Choose Single-Page App and press Enter.

Use http://localhost:3000/callback for the Redirect URI and accept the default Logout Redirect URI of http://localhost:3000.

What does the Okta CLI do?

The Okta CLI will create an OIDC Single-Page App in your Okta Org. It will add the redirect URIs you specified and grant access to the Everyone group. It will also add a trusted origin for http://localhost:3000. You will see output like the following when it’s finished:

Okta application configuration:
Issuer:    https://dev-133337.okta.com/oauth2/default
Client ID: 0oab8eb55Kb9jdMIr5d6

NOTE: You can also use the Okta Admin Console to create your app. See Create a React App for more information.

Run okta login and open the returned URL in your browser. Sign in to the Okta Admin Console, find your app in the Applications section, and edit its general settings. Enable the Implicit (Hybrid) grant type, select allow access token, and Save.

Copy your Client ID and paste it as a variable into a file called .env.local in the root of your project. This will allow you to access the file in your code without needing to store credentials in source control. You’ll also need to add your organization URL (without the -admin suffix). Environment variables (other than NODE_ENV) need to start with REACT_APP_ in order for Create React App to read them, so the file should end up looking like this:

REACT_APP_OKTA_ORG_URL=https://{yourOktaDomain}
REACT_APP_OKTA_CLIENT_ID={yourClientId}

You may need to restart your server before it will recognize these changes. You can find the running instance and then hit ctrl-c to close it. Then run it again with yarn start.

The easiest way to add Authentication with Okta to a React app is to use Okta’s React SDK. You’ll also need to add routes, which can be done using React Router. Go ahead and add these dependencies:

yarn add @okta/okta-react@1.2.0 react-router-dom@4.3.1

Now in order to use Okta in your React app, you’ll need to wrap the app in a provider. In your src/index.js, instead of rendering the <App /> component by itself, you’ll need to render a <Router> component. This uses React Router to control different URL paths in your app. Next, you’ll need to include the <Security> component as a child. This allows Okta to add an auth object to the context, which can be read from children in the React tree. Then you’ll need a couple Routes to let the router know what to display for each route. For the default route, you’ll display App, so everything should look the same as it did before. But you’ll also add an /callback route to let Okta handle the OAuth callback. Replace the call to ReactDOM.render with the following:

import { BrowserRouter as Router, Route } from 'react-router-dom';
import { Security, ImplicitCallback } from '@okta/okta-react';

ReactDOM.render(
  <Router>
    <Security
      issuer={`${process.env.REACT_APP_OKTA_ORG_URL}/oauth2/default`}
      client_id={process.env.REACT_APP_OKTA_CLIENT_ID}
      redirect_uri={`${window.location.origin}/callback`}
    >
      <Route path="/" exact component={App} />
      <Route path="/callback" component={ImplicitCallback} />
    </Security>
  </Router>,
  document.getElementById('root')
);

To tie Okta’s auth to a React component, you’ll need to wrap it in a withAuth, which adds an auth prop. Before you do that though, you can simplify things by again making use of hooks. Here I’ll have you create a custom hook that accepts the auth prop and returns the authenticated state as well as the current user. Create a new file src/auth.js that contains the following:

import { useState, useEffect } from 'react';

export const useAuth = auth => {
  const [authenticated, setAuthenticated] = useState(null);
  const [user, setUser] = useState(null);

  useEffect(() => {
    auth.isAuthenticated().then(isAuthenticated => {
      if (isAuthenticated !== authenticated) {
        setAuthenticated(isAuthenticated);
      }
    });
  });

  useEffect(() => {
    if (authenticated) {
      auth.getUser().then(setUser);
    } else {
      setUser(null);
    }
  }, [authenticated]);

  return [authenticated, user];
};

Here you’re using two pieces of state: authenticated and user, which both start out as null. Every time the component is rendered, the auth checks to see whether or not a user is authenticated. If authentication has changed, it updates the state. Initially, it will be null, but after the first time, it will always be either true or false. Whenever the authenticated state changes, that will trigger a call to get more information about the user. If the user isn’t authenticated, you can skip the check and simply set the user to null.

Back in src/App.js, you’ll need to import both the withAuth higher order component that Okta provides, and your new useAuth hook.

import { withAuth } from '@okta/okta-react';
import { useAuth } from './auth';

Wrap the App with withAuth and change the function to accept an auth param. Previously we had:

const App = () => {
  // ...
}

Now you should have:

const App = withAuth(({ auth }) => {
});

Add the new hook below the joke state:

const [authenticated, user] = useAuth(auth);

You can now add a new button after the “Get a new joke” button. When the authenticated variable is null, that means Okta hasn’t initialized yet so you’re not sure if the user is logged in or not. You could show some sort of loading indicator, or you could simply not render the button until you’re sure about the state of authentication:

{authenticated !== null && (
  <button
    onClick={() => authenticated ? auth.logout() : auth.login()}
    className="App-link"
  >
    Log {authenticated ? 'out' : 'in'}
  </button>
)}

Now the user can log in or out, but it doesn’t really do anything besides changing the button from “Log in” to “Log out”. The ICNDb API supports adding a first and last name to the URL, which replaces Chuck Norris with that name. Before the call to fetch, modify the url if there is a user:

if (user) {
  url.searchParams.set('firstName', user.given_name);
  url.searchParams.set('lastName', user.family_name);
}

Lastly, add a new effect to reset the joke whenever the user state changes. This will cause the app to fetch a new joke with the user’s name after they’ve logged in, or a default Chuck Norris joke when the user logs out.

useEffect(() => {
  setJoke('');
}, [user]);

Your final code should look like this:

import React, { useState, useEffect } from 'react';
import { decode } from 'he';
import { withAuth } from '@okta/okta-react';

import logo from './chuck-norris.png';
import './App.css';
import { useAuth } from './auth';

const App = withAuth(({ auth }) => {
  const [joke, setJoke] = useState('');
  const [authenticated, user] = useAuth(auth);

  const fetchJoke = async signal => {
    const url = new URL('https://api.icndb.com/jokes/random');
    if (user) {
      url.searchParams.set('firstName', user.given_name);
      url.searchParams.set('lastName', user.family_name);
    }
    const response = await fetch(url, { signal });
    const { value } = await response.json();

    setJoke(decode(value.joke));
  };

  useEffect(() => {
    if (!joke) {
      const controller = new AbortController();
      fetchJoke(controller.signal);

      return () => controller.abort();
    }
  }, [joke]);

  useEffect(() => {
    setJoke('');
  }, [user]);

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>{joke || '...'}</p>
        <button className="App-link" onClick={() => setJoke('')}>
          Get a new joke
        </button>
        {authenticated !== null && (
          <button
            onClick={() => authenticated ? auth.logout() : auth.login()}
            className="App-link"
          >
            Log {authenticated ? 'out' : 'in'}
          </button>
        )}
      </header>
    </div>
  );
});

export default App;

Jon Snow joke

Learn More About React and Secure User Authentication

Hopefully, you had some fun with this app and learned a bit about how easy it can be to add authentication to your React app. If you want to see the final code sample for reference, you can find it on GitHub.

For more examples using Okta with React, check out some of these other posts, or browse the Okta Developer Blog.

If you have any questions about this post, please add a comment below. For more awesome content, follow @oktadev on Twitter, like us on Facebook, or subscribe to our YouTube channel.

Okta Developer Blog Comment Policy

We welcome relevant and respectful comments. Off-topic comments may be removed.