Tutorial

How To Enforce Authentication and Route Protection for a React and GraphQL App

Draft updated on Invalid Date
author

Chris Nwamba

How To Enforce Authentication and Route Protection for a React and GraphQL App

This tutorial is out of date and no longer maintained.

Introduction

Email, Facebook, Google, Twitter, Github are all possible options for authenticating users in your web apps. Apps built with React and GraphQL are no less candidates for such authentications.

In this article, you will learn how to add varieties of authentication providers to a GraphQL app using:

  • GraphQL: A query language for your API
  • Graphcool: GraphQL backend as a service
  • Auth0: Authentication as a service
  • React: JavaScript library for building user interfaces

We will also learn how to protect React routes from being accessed if the user fails to be authenticated by our authentication server.

Step 1 — Preparing the Project

The plan is to have a React project setup. In the same directory where the React project is set up, we can configure a Graphcool server and tell our project how we want our server to behave. Start with installing the create-react-app and graphcool-framework CLI tools:

  1. npm install -g create-react-app graphcool-framework

Then use the React CLI tool to scaffold a new React app:

  1. create-react-app do-auth

Create the Graphcool server by moving into the React app you just created and running the Graphcool init command:

  1. cd do-auth
  1. graphcool-framework init server

Step 2 — Creating React Routes and UIs

You need both public and protected routes:

  • A home page (public)
  • A profile page (protected)
  • An admin page (protected and available to only admins)
  • An about page (public)

Create a containers folder and add home.js, profile.js, admin.js, and about.js as files to represent each of these routes.

Home.js

import React from 'react';
import Hero from '../components/hero';

const Home = (props) => (
     <div>
        <Hero page="Home"></Hero>
        <h2>Home page</h2>
      </div>
    )
    export default Home;

About.js

import React from 'react';
import Hero from '../components/hero';

const About = (props) => (
     <div>
        <Hero page="About"></Hero>
        <h2>About page</h2>
      </div>
    )
    export default About;

Profile.js

import React from 'react';
import Hero from '../components/hero';

const Profile = props => (
     <div>
        <Hero page="Profile"></Hero>
        <h2>Profile page</h2>
      </div>
    );
    export default Profile;

Admin.js

import React from 'react';
import Hero from '../components/hero';

const Admin = (props) => (
     <div>
        <Hero></Hero>
        <Hero page="Admin"></Hero>
      </div>
    )
    export default Admin;

Each of the pages imports and uses Hero to display a landing hero message. Create a components folder and create a Hero.js file with the following:

import React from 'react';
import Nav from './nav'
import './hero.css'

const Hero = ({ page }) => (
     <section className="hero is-large is-dark">
        <div className="hero-body">
        <Nav></Nav>
          <div className="container">
            <h1 className="title">DO Auth</h1>
            <h2 className="subtitle">Welcome to the Auth {page}</h2>
          </div>
        </div>
      </section>
    );
    export default Hero;

You can add the navigation component as nav.js in the components folder as well. Before we do that, we need to set up routing for the React app and have the pages exposed as routes.

Start with installing the React Router library:

  1. yarn add react-router-dom

Next, provide the router to the App through the index.js entry file:

    //...
import { BrowserRouter } from 'react-router-dom';
import App from './App';
    //...

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

Then configure the routes in the App component:

    import React, { Component } from 'react';
    import { Switch, Route } from 'react-router-dom';

    import Profile from './containers/profile';
    import About from './containers/about';
    import Admin from './containers/admin';
    import Home from './containers/home';

    class App extends Component {
      render() {
        return (
          <div className="App">
            <Switch>
              <Route exact path="/" component={Home} />
              <Route exact path="/about" component={About} />
              <Route exact path="/profile" component={Profile} />
              <Route exact path="/admin" component={Admin} />
            </Switch>
          </div>
        );
      }
    }
    export default App;

Back to the navigation component (nav.js), we want to use the Link component from react-router-dom to provision navigation:

    import React from 'react';
    import { Link } from 'react-router-dom'
    import './nav.css'
    const Nav = () => {
      return (
        <nav className="navbar">
          <div className="navbar-brand">
            <Link className="navbar-item" to="/">
              <strong>Auth Page</strong>
            </Link>
          </div>
          <div className="navbar-menu">
            <div className="navbar-end">
              <Link to="/about" className="navbar-item">
                About
              </Link>
              <Link to="/profile" className="navbar-item">
                Profile
              </Link>
              <div className="navbar-item join">
                Join
              </div>
            </div>
          </div>
        </nav>
      );
    };
    export default Nav;

Next, create a Graphcool Server

  1. graphcool-framework deploy

Step 3 — Configuring Auth0 for Server Authentication

Let’s step away from the client app for a second and get back to the Graphcool server we created earlier. Graphcool has a serverless function concept that allows you to extend the functionalities of your server. This feature can be used to achieve a lot of 3rd party integrations including authentication.

Some functions for such integrations have been pre-packaged for you so you don’t have to create them from scratch. You only need to install the template, uncomment some configurations and types, then update or tweak the code as you wish.

Let’s add the Auth0 template. Make sure you are in the server folder and run the following:

  1. graphcool-framework add-template graphcool/templates/auth/auth0

This will create an auth0 folder in server/src. This folder contains both the function logic, types, and the mutation definition that triggers this function.

Next, you need to create an Auth0 API and then add the API’s configuration to your server. Create an account first, then create a new API from your API dashboard. You can name the API whatever you like. Provide an identifier that is unique to all your existing APIs.

Uncomment the template configuration in server/src/graphcool.yml and update it to look like this:

    authenticate:
        handler:
          code:
            src: ./src/auth0/auth0Authentication.js
            environment:
              AUTH0_DOMAIN: [YOUR AUTH0 DOMAIN]
              AUTH0_API_IDENTIFIER: [YOUR AUTH0 IDENTIFIER]
        type: resolver
        schema: ./src/auth0/auth0Authentication.graphql

AUTH0_DOMAIN and AUTH0_API_IDENTIFIER will be exposed on process.env in your function as environmental variables.

The template command also generates a type in server/src/types.graphql. It’s commented out by default. You need to uncomment the following:

    type User @model {
      # Required system field:
      id: ID! @isUnique # read-only (managed by Graphcool)
      # Optional system fields (remove if not needed):
      createdAt: DateTime! # read-only (managed by Graphcool)
      updatedAt: DateTime! # read-only (managed by Graphcool)
      email: String
      auth0UserId: String @isUnique
    }

You need to remove the User type that was generated when the server was created so that this auth User type can replace it.

Next, you need to make some tweaks to the authentication logic. Find the following code block:

    jwt.verify(
            token,
            signingKey,
            {
              algorithms: ['RS256'],
              audience: process.env.AUTH0_API_IDENTIFIER,
              ignoreExpiration: false,
              issuer: `https://${process.env.AUTH0_DOMAIN}/`
            },
            (err, decoded) => {
              if (err) throw new Error(err)
              return resolve(decoded)
            }
          )

And update the audience property in the verify method to aud:

    jwt.verify(
            token,
            signingKey,
            {
              algorithms: ['RS256'],
              aud: process.env.AUTH0_API_IDENTIFIER,
              ignoreExpiration: false,
              issuer: `https://${process.env.AUTH0_DOMAIN}/`
            },
            (err, decoded) => {
              if (err) throw new Error(err)
              return resolve(decoded)
            }
          )

Lastly, the auth token will always encode an email so there is no need to do the following to get the email:

    let email = null
    if (decodedToken.scope.includes('email')) {
      email = await fetchAuth0Email(accessToken)
    }

We can get the email from the decodedToken right away:

const email = decodedToken.email

This makes the fetchAuth0Email function useless, so you can remove it.

Deploy the server to Graphcool by running the following:

  1. graphcool-framework

If it’s your first time using Graphcool, you should be taken to a page to create a Graphcool account.

Step 4 — Configuring Auth0 for Client Authentication

Your server is set to receive tokens for authentication. You can test this out in the Graphcool playground by running the following command:

  1. graphcool-framework playground

We supply the mutation an Auth0 token and we get a node token from Graphcool. Let’s see how we can get tokens from Auth0.

Just like creating an API, you also need to create a client for the project. The client is used to trigger authentication from the browser. On your Auth0 dashboard navigation, click clients and create a new client.

The application type should be set to Single Page Web Applications which is what a routed React app is.

When auth is initiated, it redirects to your Auth0 domain to verify the user. When the user is verified, it needs to redirect the user back to your app. The callback URL is where it comes back to after redirecting. Go to the Settings tab of the client you just created and set the callback URL:

You are now done with setting up a client configuration on the Auth0 dashboard. The next thing we want to do is create a service in our React app that exposes a few methods. These methods will handle utility tasks like triggering authentication, handling response from Auth0, logging out, etc.

First, install the Auth0 JS library:

  1. yarn add auth0-js

Then create a services folder in src. Add an auth.js in the new folder with the following content:

    import auth0 from 'auth0-js';

    export default class Auth {
      auth0 = new auth0.WebAuth({
        domain: '[Auth0 Domain]',
        clientID: '[Auth0 Client ID]',
        redirectUri: 'http://localhost:3000/callback',
        audience: '[Auth0 Client Audience]',
        responseType: 'token id_token',
        scope: 'openid profile email'
      });
      handleAuthentication(cb) {
        this.auth0.parseHash({hash: window.location.hash}, (err, authResult) => {
          if (authResult && authResult.accessToken && authResult.idToken) {
            this.auth0.client.userInfo(authResult.accessToken, (err, profile) => {
              this.storeAuth0Cred(authResult, profile);
              cb(false, {...authResult, ...profile})
            });
          } else if (err) {
            console.log(err);
            cb(true, err)
          }
        });
      }
      storeAuth0Cred(authResult, profile) {
        // Set the time that the access token will expire at
        let expiresAt = JSON.stringify(
          authResult.expiresIn * 1000 + new Date().getTime()
        );
        localStorage.setItem('do_auth_access_token', authResult.accessToken);
        localStorage.setItem('do_auth_id_token', authResult.idToken);
        localStorage.setItem('do_auth_expires_at', expiresAt);
        localStorage.setItem('do_auth_profile', JSON.stringify(profile));
      }
      storeGraphCoolCred(authResult) {
        localStorage.setItem('do_auth_gcool_token', authResult.token);
        localStorage.setItem('do_auth_gcool_id', authResult.id);
      }
      login() {
        this.auth0.authorize();
      }
      logout(history) {
        // Clear access token and ID token from local storage
        localStorage.removeItem('do_auth_access_token');
        localStorage.removeItem('do_auth_id_token');
        localStorage.removeItem('do_auth_expires_at');
        localStorage.removeItem('do_auth_profile');
        localStorage.removeItem('do_auth_gcool_token');
        localStorage.removeItem('do_auth_gcool_id');
        // navigate to the home route
        history.replace('/');
      }
      isAuthenticated() {
        // Check whether the current time is past the
        // access token's expiry time
        const expiresAt = JSON.parse(localStorage.getItem('do_auth_expires_at'));
        return new Date().getTime() < expiresAt;
      }
      getProfile() {
        return JSON.parse(localStorage.getItem('do_auth_profile'));
      }
    }

Here’s what this code does:

  • First, this creates an instance of the Auth0 SDK and configures it using your Auth0 client credentials. This instance is then stored in the instance variable, auth0.
  • handleAuthentication will be called by one of your components when authentication is completed. Auth0 will pass the token back to you through URL hash. This method reads and passes this hash.
  • storeAuth0Cred and storeGraphCoolCred will persist your credentials to localStorage for future use.
  • You can call isAuthenticated to check if the token stored in localStorage is still valid.
  • getProfile returns the JSON payload of the user’s profile.

You also want to redirect the user to your profile page if they are authenticated, or send them back to the home page (which is the default page) if they are not. The Callback page is the best candidate for this. First, add another route to the routes in App.js:

    //...
    import Home from './containers/home';
    import Callback from './containers/callback'
    class App extends Component {
      render() {
        return (
          <div className="App">
            <Switch>
              <Route exact path="/" component={Home} />
              {/* Callback route */}
              <Route exact path="/callback" component={Callback} />
              ...
            </Switch>
          </div>
        );
      }
    }
    export default App;

/callback uses the Callback component which you need to create in src/container/Callback.js:

    import React from 'react';
    import { graphql } from 'react-apollo';
    import gql from 'graphql-tag';
    import Auth from '../services/auth'
    const auth = new Auth();
    class Callback extends React.Component {
      componentDidMount() {
        auth.handleAuthentication(async (err, authResult) => {
          // Failed. Send back home
          if (err) this.props.history.push('/');
          // Send mutation to Graphcool with idToken
          // as the accessToken
          const result = await this.props.authMutation({
            variables: {
              accessToken: authResult.idToken
            }
          });
          // Save response to localStorage
          auth.storeGraphCoolCred(result.data.authenticateUser);
          // Redirect to profile page
          this.props.history.push('/profile');
        });
      }
      render() {
        // Show a loading text while the app validates the user
        return <div>Loading...</div>;
      }
    }

    // Mutation query
    const AUTH_MUTATION = gql`
      mutation authMutation($accessToken: String!) {
        authenticateUser(accessToken: $accessToken) {
          id
          token
        }
      }
    `;

This uses the handleAuthentication method exposed by Auth to send a mutation to the Graphcool server. You have not yet set up a connection to the server, but once you do and attempt to authenticate a user, this callback page will send a write mutation to the Graphcool server to tell the server that the user exists and is allowed to access resources.

Step 5 — Setting Up Apollo and Connecting to the Server

In the Callback component, you used the graphql (which you have not yet installed) to connect the component to a mutation. This doesn’t imply that there is a connection to the Graphcool server yet. You need to set up this connection using Apollo and then provide the Apollo instance at the top level of your app.

Start with installing the required dependencies:

  1. yarn add apollo-client-preset react-apollo graphql-tag graphql

Update the src/index.js entry file:

    import React from 'react';
    import ReactDOM from 'react-dom';
    import { BrowserRouter } from 'react-router-dom';
    import './index.css';
    import App from './App';
    import registerServiceWorker from './registerServiceWorker';

    // Import modules
    import { ApolloProvider } from 'react-apollo';
    import { ApolloClient } from 'apollo-client';
    import { HttpLink } from 'apollo-link-http';
    import { InMemoryCache } from 'apollo-cache-inmemory';

    // Create connection link
    const httpLink = new HttpLink({ uri: '[SIMPLE API URL]' });

    // Configure client with link
    const client = new ApolloClient({
      link: httpLink,
      cache: new InMemoryCache()
    });

    // Render App component with Apollo provider
    ReactDOM.render(
      <BrowserRouter>
        <ApolloProvider client={client}>
          <App />
        </ApolloProvider>
      </BrowserRouter>,
      document.getElementById('root')
    );

    registerServiceWorker();

First, this imports all the dependencies. Then this created a link using HttpLink. The argument passed is an object with the URI. You can get your server’s URI by running the following command on the server folder:

  1. graphcool-framework list

Use the Simple URI to replace the placeholder in the code above.

Next, you created and configured an Apollo client instance with this link as well as a cache. This created client instance is now passed as prop to the Apollo provider which wraps the App component.

Step 6 — Testing the Authentication Flow

Now that everything is intact, you can add an event to the button on our navigation bar to trigger the auth process:

    //...
    import Auth from '../services/auth'
    const auth = new Auth();
    const Nav = () => {
      return (
        <nav className="navbar">
          ...
          <div className="navbar-menu">
            <div className="navbar-end">
              ...
              <div className="navbar-item join" onClick={() => {auth.login()}}>
                Join
              </div>
            </div>
          </div>
        </nav>
      );
    };
    export default Nav;

When the Join button is clicked, we trigger the auth.login method which would redirect us to our Auth0 domain for authentication.

After the user logs in, Auth0 will ask if the user wants to access their profile information.

After authentication, watch the page move back to /callback and the /profile if authentication was successful.

You can also confirm that the user is created by going to the data view in your Graphcool dashboard and opening the Users table.

Step 7 — Adding Conditional Buttons

Next you will want to hide the login button when the user is authenticated and show the logout button rather. Back in the nav.js replace the Join button element with this conditional logic:

     {auth.isAuthenticated() ? (
        <div
          className="navbar-item join"
          onClick={() => {
            auth.logout();
          }}
        >
          Logout
        </div>
      ) : (
        <div
          className="navbar-item join"
          onClick={() => {
            auth.login();
          }}
        >
          Join
        </div>
      )}

The Auth services exposes a method called isAuthenticated to check if the token is stored in the localStorage and not expired. Your user will then be logged in.

Step 8 — Displaying User’s Profile

You can also use the Auth service to retrieve a logged in user profile. This profile is already available in the localStorage:

    //...
    import Auth from '../services/auth';
    const auth = new Auth();
    const Profile = props => (
      <div>
        //...
        <h2 className="title">Nickname: {auth.getProfile().nickname}</h2>
      </div>
    );
    export default Profile;

The nickname will then get printed in the browser.

Step 9 — Securing the Graphcool Endpoint

At this time, an authenticated user will still have access to a restricted backend because you haven’t told the server about the token.

You can send the token as Bearer token in the header of our request. Update the index.js with the following:

    //...
    import { ApolloLink } from 'apollo-client-preset'
    const httpLink = new HttpLink({ uri: '[SIMPLE URL]' });

    const middlewareAuthLink = new ApolloLink((operation, forward) => {
      const token = localStorage.getItem('do_auth_gcool_token')
      const authorizationHeader = token ? `Bearer ${token}` : null
      operation.setContext({
        headers: {
          authorization: authorizationHeader
        }
      })
      return forward(operation)
    })
    const httpLinkWithAuthToken = middlewareAuthLink.concat(httpLink)

Instead of creating the Apollo client with just the HTTP Link, you update it to use the middleware you just created which adds a token to all server requests we make:

    const client = new ApolloClient({
      link: httpLinkWithAuthToken,
      cache: new InMemoryCache()
    })

You can then use the token at the server’s end to validate the request.

Step 10 — Protecting the Routes

In as much as you have done a great job by securing the most important part of your project which is the server and its data, it doesn’t make sense to leave the user hanging at a route where there will be no content. The /profile route needs to be protected from being accessed when the user is not authenticated.

Update App.js to redirect to the home page if the user goes to profile but is not authenticated:

    //...
    import { Switch, Route, Redirect } from 'react-router-dom';
    //...
    import Auth from './services/auth';
    const auth = new Auth();
    class App extends Component {
      render() {
        return (
          <div className="App">
            <Switch>
              <Route exact path="/" component={Home} />
              <Route exact path="/callback" component={Callback} />
              <Route exact path="/about" component={About} />
              <Route
                exact
                path="/profile"
                render={props =>
                  auth.isAuthenticated() ? (
                    <Profile />
                  ) : (
                    <Redirect
                      to={{
                        pathname: '/'
                      }}
                    />
                  )
                }
              />
            </Switch>
          </div>
        );
      }
    }
    export default App;

You are still using auth.isAuthenticated to check for authentication. If it returns true, /profile wins, else, / wins.

Conclusion

In this tutorial, you authenticated a user using Auth0 in a GraphQL project. What you can do is go to your Auth0 dashboard and add a few more social authentication options like Twitter. You will be asked for your Twitter developer credentials which can get from the Twitter Developer website.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us


About the authors
Default avatar
Chris Nwamba

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Featured on Community

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel