Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

ReactJS Authentication Tutorial, Part 3

DZone's Guide to

ReactJS Authentication Tutorial, Part 3

In the third and final part of our series, we look at how ReactJS can be used with Auth0 to create authentication requests from your users.

· Web Dev Zone ·
Free Resource

Deploy code to production now. Release to users when ready. Learn how to separate code deployment from user-facing feature releases with LaunchDarkly.

Welcome back! If you missed Part 1 or Part 2, follow the links to check them out! 

Adding Authentication to Your ReactJS App

The majority of the apps we use on a daily basis have a means of authenticating users. I'll show you how to easily add authentication to our ReactJS application. We'll use Auth0 as our authentication service.

Auth0 allows us to issue JSON Web Tokens (JWTs). If you don't already have an Auth0 account, sign up for a free one now.

Login to your Auth0 management dashboard and create a new API client. If you don't already have the APIs menu item, you can enable it by going to your Account Settings and in the Advanced tab, scroll down until you see Enable APIs Section and flip the switch.

From here, click on the APIs menu item and then the Create API button. You will need to give your API a name and an identifier. The name can be anything you choose, so make it as descriptive as you want. The identifier will be used to identify your API, this field cannot be changed once set. For our example, I'll name the API Chuck Norris World API and for the identifier, I'll set it as http://chucknorrisworld.com. We'll leave the signing algorithm as RS256 and click on the Create API button.

Creating the Chuck Norris World APICreating the Chuck Norris World API

Next, let's define some scopes for our API. Scopes allow us to manage access to our API. We can define as few or as many scopes as we want. For our simple example, we'll just create a single scope that will grant users full access to the API.

Locate scopes barLocate Scopes bar

Adding Scope to APIAdding scope

Secure the Node API

We need to secure the API so that the celebrity endpoint will only be accessible to authenticated users. We can secure it easily with Auth0.

Open up your server.js file and replace the YOUR-API-AUDIENCE-ATTRIBUTE, and YOUR-AUTH0-DOMAIN variables with the audience attribute of the API, and your Auth0 domain respectively. Then add the authCheck middleware to the celebrity endpoint like so:

app.get('/api/jokes/celebrity', authCheck, (req,res) => {
  let CelebrityJokes = [
  {
    id: 88881,
    joke: 'As President Roosevelt said: "We have nothing to fear but fear itself. And Chuck Norris."'
  },
  {
    id: 88882,
    joke: "Chuck Norris only let's Charlie Sheen think he is winning. Chuck won a long time ago."
  },
  {
    id: 88883,
    joke: 'Everything King Midas touches turnes to gold. Everything Chuck Norris touches turns up dead.'
  },
  {
    id: 88884,
    joke: 'Each time you rate this, Chuck Norris hits Obama with Charlie Sheen and says, "Who is winning now?!"'
  },
  {
    id: 88885,
    joke: "For Charlie Sheen winning is just wishful thinking. For Chuck Norris it's a way of life."
  },
  {
    id: 88886,
    joke: "Hellen Keller's favorite color is Chuck Norris."
  } 
  ];
  res.json(CelebrityJokes);
})

app.listen(3333);
console.log('Listening on localhost:3333');

Note: You should load these values from environment variables for security reasons. No one should have access to your Auth0 secret.

Try accessing the http://localhost:3333/api/jokes/celebrity endpoint again from Postman. You should be denied access like so:

Unauthorized AccessUnauthorized Access

Next, let's add authentication to our front-end.

Adding Authentication to Our ReactJS Front-End

We'll create an authentication service to handle everything about authentication in our app. Go ahead and create an AuthService.js file inside the utilsdirectory.

Before we add code, you need to install jwt-decode and auth0-js node packages like so:

npm install jwt-decode auth0-js --save

Open up the AuthService.js file and add code to it like so:

import decode from 'jwt-decode';
import { browserHistory } from 'react-router';
import auth0 from 'auth0-js';
const ID_TOKEN_KEY = 'id_token';
const ACCESS_TOKEN_KEY = 'access_token';

const CLIENT_ID = '{AUTH0_CLIENT_ID}';
const CLIENT_DOMAIN = 'AUTH0_DOMAIN';
const REDIRECT = 'YOUR_CALLBACK_URL';
const SCOPE = 'YOUR_SCOPE';
const AUDIENCE = 'AUDIENCE_ATTRIBUTE';

var auth = new auth0.WebAuth({
  clientID: CLIENT_ID,
  domain: CLIENT_DOMAIN
});

export function login() {
  auth.authorize({
    responseType: 'token id_token',
    redirectUri: REDIRECT,
    audience: AUDIENCE,
    scope: SCOPE
  });
}

export function logout() {
  clearIdToken();
  clearAccessToken();
  browserHistory.push('/');
}

export function requireAuth(nextState, replace) {
  if (!isLoggedIn()) {
    replace({pathname: '/'});
  }
}

export function getIdToken() {
  return localStorage.getItem(ID_TOKEN_KEY);
}

export function getAccessToken() {
  return localStorage.getItem(ACCESS_TOKEN_KEY);
}

function clearIdToken() {
  localStorage.removeItem(ID_TOKEN_KEY);
}

function clearAccessToken() {
  localStorage.removeItem(ACCESS_TOKEN_KEY);
}

// Helper function that will allow us to extract the access_token and id_token
function getParameterByName(name) {
  let match = RegExp('[#&]' + name + '=([^&]*)').exec(window.location.hash);
  return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
}

// Get and store access_token in local storage
export function setAccessToken() {
  let accessToken = getParameterByName('access_token');
  localStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
}

// Get and store id_token in local storage
export function setIdToken() {
  let idToken = getParameterByName('id_token');
  localStorage.setItem(ID_TOKEN_KEY, idToken);
}

export function isLoggedIn() {
  const idToken = getIdToken();
  return !!idToken && !isTokenExpired(idToken);
}

function getTokenExpirationDate(encodedToken) {
  const token = decode(encodedToken);
  if (!token.exp) { return null; }

  const date = new Date(0);
  date.setUTCSeconds(token.exp);

  return date;
}

function isTokenExpired(token) {
  const expirationDate = getTokenExpirationDate(token);
  return expirationDate < new Date();
}

In the code above, we are using a hosted version of Auth0 Lock in the loginmethod and passed in our credentials.

The Auth0 package calls the Auth0's authorize endpoint. With all the details we passed to the method, our client app will be validated and authorized to perform authentication. You can learn more about the specific values that can be passed to the authorize method here.

The parameters that you do not have yet are the {AUTH0_CLIENT_ID} and the {YOUR_CALLBACK_URL}. When you created your API, Auth0 also created a test client which you can use. Additionally, you can use any existing SPA Auth0 client found in Clients section of your management dashboard.

Check the Test panel of your API from the dashboard. You'll see the test client like so:

Chuck Norris World ClientChuck Norris World API Client

Now, go to the clients area and check for the test client. You should see it in your list of clients like so:

Chuck Norris World Test Client

Open the client and change the Client Type to Single Page Application.

Non-interactive clients are meant to be used in machine-to-machine interactions. We are using an SPA to interact with the API so the client should be an SPA client. Check out Implicit Grant and client credentials exchange for more information.

Let's quickly go ahead to change the title of the client to Chuck Norris Worldlike so:

Client Name Change

Changing the Client name is totally optional.

Copy the CLIENT ID and replace it with the value of AUTH0_CLIENT_ID in the variable CLIENT_ID. Replace your callback URL with http://localhost:3000/callback. Don't forget to add that to the Allowed Callback URLs and http://localhost:3000 to the Allowed Origins (CORS).

We also checked whether the token has expired via the getTokenExpirationDate and isTokenExpired methods. The isLoggedInmethod returns true or false based on the presence and validity of a user id_token.

Finally, we implemented a middleware, the requireAuth method. We'll use this method to protect the /special route from being accessed for non-logged in users.

Let's go update the Nav component to hide/show the login and logoutbuttons based on the user's authentication status.

Now, your Nav component should look like this:

import React, { Component } from 'react';
import { Link } from 'react-router';
import { login, logout, isLoggedIn } from '../utils/AuthService';
import '../App.css';

class Nav extends Component {

  render() {
    return (
      <nav className="navbar navbar-default">
        <div className="navbar-header">
          <Link className="navbar-brand" to="/">Chuck Norris World</Link>
        </div>
        <ul className="nav navbar-nav">
          <li>
            <Link to="/">Food Jokes</Link>
          </li>
          <li>
            { 
             ( isLoggedIn() ) ? <Link to="/special">Celebrity Jokes</Link> :  ''
            }

          </li>
        </ul>
        <ul className="nav navbar-nav navbar-right">
          <li>
           { 
             (isLoggedIn()) ? ( <button className="btn btn-danger log" onClick={() => logout()}>Log out </button> ) : ( <button className="btn btn-info log" onClick={() => login()}>Log In</button> )
           }
          </li>
        </ul>
      </nav>
    );
  }
}

export default Nav;

Nav.js

Note: We used an arrow function to wrap and execute the onClick handlers like so: {() => login()}. Check out how to handle events in react with arrow function to understand why we used arrow functions.

We imported login, logout and isLoggedIn functions from the AuthService. Then, we attached the login() and logout() functions to the login and logout buttons respectively.

We also hid the /special link by checking the authentication status of the user via the isLoggedIn() function.

Open up the FoodJokes Component and modify it like so:

import React, { Component } from 'react';
import { Link } from 'react-router';
import Nav from './Nav';
import { isLoggedIn } from '../utils/AuthService';
import { getFoodData } from '../utils/chucknorris-api';

class FoodJokes extends Component {

  constructor() {
    super()
    this.state = { jokes: [] };
  }

  getFoodJokes() {
    getFoodData().then((jokes) => {
      this.setState({ jokes });
    });
  }

  componentDidMount() {
    this.getFoodJokes();
  }

  render() {

    const { jokes }  = this.state;

    return (
      <div>
        <Nav />
        <h3 className="text-center">Chuck Norris Food Jokes</h3>
        <hr/>

        { jokes.map((joke, index) => (
              <div className="col-sm-6" key={index}>
                <div className="panel panel-primary">
                  <div className="panel-heading">
                    <h3 className="panel-title"> <span className="btn">#{ joke.id }</span></h3>
                  </div>
                  <div className="panel-body">
                    <p> { joke.joke } </p>
                  </div>
                </div>
              </div>
          ))}

        <div className="col-sm-12">
          { isLoggedIn() ?
          <div className="jumbotron text-center">
            <h2>View Celebrity Jokes</h2>
            <Link className="btn btn-lg btn-success" to='/special'> Celebrity Jokes </Link>
          </div> : <div className="jumbotron text-center"><h2>Get Access to Celebrity Jokes By Logging In</h2></div>
          }
        </div>
      </div>
    );
  }
}

export default FoodJokes;

We are enabling the link to celebrity jokes based on the login status of a user via the isLoggedIn() method.

Add a Callback Component

We will create a new component and call it Callback.js. This component will be activated when the localhost:3000/callback route is called and it will process the redirect from Auth0 and ensure we received the right data back after a successful authentication. The component will store the access_token and id_token.

import { Component } from 'react';
import { setIdToken, setAccessToken } from '../utils/AuthService';

class Callback extends Component {

  constructor() {
    super()
  }

  componentDidMount() {
    setAccessToken();
    setIdToken();
    window.location.href = "/";
  }

  render() {
    return null;
  }
}

export default Callback;

Callback.js

Once a user is authenticated, Auth0 will redirect back to our application and call the /callback route. Auth0 will also append the id_token as well as the access_token to this request, and our Callback component will make sure to properly process and store those tokens in localStorage. If all is well, meaning we received an id_token, and access_token, we will be redirected back to the /page and will be in a logged-in state.

Add Some Values to Auth0 Dashboard

Just before you try to log in or sign up, head over to your Auth0 dashboard and add http://localhost:3000/callback to the Allowed Callback URLs and http://localhost:3000 to Allowed Origins (CORS).

Secure the Special Route

We need to ensure that no one can go to the browser and just type /special to access the celebrity route.

Open up index.js and add an onEnter prop with a value of requireAuth to the /special route like so:

....
....
import { requireAuth } from './utils/AuthService';

const Root = () => {
  return (
    <div className="container">
      <Router history={browserHistory}>
        <Route path="/" component={FoodJokes}/>
        <Route path="/special" component={CelebrityJokes} onEnter={requireAuth} />
      </Router>
    </div>
  )
}

index.js

Just one more thing before we test the app. Register the /callback route in the routes file like so:

import React from 'react';
import ReactDOM from 'react-dom';
import CelebrityJokes from './components/CelebrityJokes';
import FoodJokes from './components/FoodJokes';
import Callback from './components/Callback';
import { Router, Route, browserHistory } from 'react-router';
import { requireAuth } from './utils/AuthService';

const Root = () => {
  return (
    <div className="container">
      <Router history={browserHistory}>
        <Route path="/" component={FoodJokes}/>
        <Route path="/special" component={CelebrityJokes} onEnter={requireAuth} />
        <Route path="/callback" component={Callback} />
      </Router>
    </div>
  )
}

ReactDOM.render(<Root />, document.getElementById('root'));

Now, try to log in.

Lock Login WidgetHosted Lock Login Widget

For the first time, the user will be shown a user consent dialog that will show the scope available. Once a user authorizes, it goes ahead to login the user and give them access based on the scopes.

User consent dialogUser presented with an option to authorize

Note: Since we are using localhost for our domain, once a user logs in for the first time, subsequent logins will not need a user consent authorization dialog. This consent dialog will not be displayed if you are using a non-localhost domain, and the client is a first-party client.

Logged In and Unauthorized to see the celebrity contentLogged In, but unauthorized to see the celebrity content

Oops! We have successfully logged in but the content of the celebrity jokes is not showing up and in the console, we are getting a 401 Unauthorized error. Why?

It's simple! We secured our endpoint earlier, but right now we are not passing the access_token to the backend yet. We need to send the access token along with our request as a header to enable the secured endpoint's recognition of the logged-in user.

Updating the Chucknorris API Helper

Go ahead and open up the utils/chucknorris-api.js file. We will tweak the getCelebrityData function a bit. Currently, it initiates a GET request only to fetch data from the API.

Now, we will pass an option to send an Authorization header with a Bearer access token along with the GET request like so:

...
import { getAccessToken } from './AuthService';


function getCelebrityData() {
  const url = `${BASE_URL}/api/jokes/celebrity`;
  return axios.get(url, { headers: { Authorization: `Bearer ${getAccessToken()}` }}).then(response => response.data);
}

The /api/jokes/celebrity endpoint will receive the token in the header and validate the user. If it is valid, the content will be provided to us.

Now, try to log in again.

Working Chuck Norris World AppWorking Chuck Norris World App

Everything is working fine. Pat yourself on the back. You have just successfully built a ReactJS app and added authentication to it!

Conclusion

ReactJS is an awesome front-end library to employ in building your user interfaces. It takes advantage of the Virtual DOM, it is fast, and it has a bubbling community. There are several React plugins/addons that the community provides to allow you do almost anything in ReactJS.

In addition, Auth0 can help secure your ReactJS apps with more than just username-password authentication. It provides features like multifactor auth, anomaly detection, enterprise federation, single sign on (SSO), and more. 

Deploy code to production now. Release to users when ready. Learn how to separate code deployment from user-facing feature releases with LaunchDarkly.

Topics:
web dev ,reactjs ,authentication ,login

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}