All in One Authentication and Route Protection for a React + GraphQL App

Email, Facebook, Google, Twitter, Github and the list can go as long as you wish. These are possible options for authenticating users in your web apps. Apps built with React and GraphQL are no less candidates for such authentications.

Our Tools

In this article, we 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.

Helpful Reading

This tutorial assumes a fair knowledge of GraphQL and React as well as how authentication flow works. You can refer to my previous post on Building a Chat App with GraphQL and React to learn a few concepts about GraphQL. You can also learn about JWT authentication from one of our previous posts.

Getting Started

The plan is to have a React project setup. In the same directory where the React project is setup, 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:

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

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

# scotch-auth is the name of our project

create-react-app scotch-auth

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

# Move into the project folder

cd scotch-auth


# Create a Graphcool server
# server is the name of the graphcool server folder
graphcool-framework init server

Create React Routes and UIs

We need a few routes — both public and protected:

  • 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 page imports and uses Hero to display a landing hero message. Create a components folder and create Hero.js 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">Scotch Auth</h1>
            <h2 className="subtitle">Welcome to the Scotch 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 setup routing for the React app and have the pages exposed as routes.

Start with installing the React Router library:

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>Scotch Auth</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;

This is what the app should look like at this stage:

Create a Graphcool Server

graphcool-framework deploy

Auth0 for Authentication: Server

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 configuration 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:

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.

Create and Add Credentials

We need to create an Auth0 API and then add the API’s configuration to our server. Create an account first, then create a new API from you API dasboard. You can name the API what ever you like. Provide an identifier that is unique to all your existing APIs (URLs are mostly recommended):

Uncomment Config 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.

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

    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.

Update Code We 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 doing 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 — you can remove it.

Deploy the server to Graphcool by running the following:

graphcool-framework

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

Auth0 for Authentication: Client

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

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.

Create client Just like creating an API, we also need to create a client for the project. The client is used to trigger authentication from the browser.

Create a Client 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.

Configure callback url 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:

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

First install the Auth0 JS library:

yarn add auth0-js

Then create a services folder in src. Add a 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('scotch_auth_access_token', authResult.accessToken);
        localStorage.setItem('scotch_auth_id_token', authResult.idToken);
        localStorage.setItem('scotch_auth_expires_at', expiresAt);
        localStorage.setItem('scotch_auth_profile', JSON.stringify(profile));
      }
      storeGraphCoolCred(authResult) {
        localStorage.setItem('scotch_auth_gcool_token', authResult.token);
        localStorage.setItem('scotch_auth_gcool_id', authResult.id);
      }
      login() {
        this.auth0.authorize();
      }
      logout(history) {
        // Clear access token and ID token from local storage
        localStorage.removeItem('scotch_auth_access_token');
        localStorage.removeItem('scotch_auth_id_token');
        localStorage.removeItem('scotch_auth_expires_at');
        localStorage.removeItem('scotch_auth_profile');
        localStorage.removeItem('scotch_auth_gcool_token');
        localStorage.removeItem('scotch_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('scotch_auth_expires_at'));
        return new Date().getTime() < expiresAt;
      }
      getProfile() {
        return JSON.parse(localStorage.getItem('scotch_auth_profile'));
      }
    }

Let me take some time to walk you through what’s going on:

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

Callback page We want to redirect the user to our 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 we 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
        }
      }
    `;
// Expose mutation query on `this.props` eg: this.props.authMutation(...)
export default graphql(AUTH_MUTATION, { name: 'authMutation' })(Callback);

It uses the handleAuthentication method exposed by Auth to send a mutation to the Graphcool server. We are yet to setup a connection to the server but once we 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.

Setup Apollo and Connect to Server

In the Callback component, we used the graphql (which we are yet to install) to connect the component to a mutation. This doesn’t imply that there is a connection to the Graphcool server yet. We need to setup this connection using Apollo and then provide the Apollo instance at the top level of our app.

Start with installing the required dependencies:

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 we imported all the dependencies, then 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:

graphcool-framework list

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

Next we 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.

Test Authentication Flow

Now that everything seems intact, let’s 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 would confirm from the user if (s)he wants you to access his/her 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:

Conditional Buttons

We should 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. Here is an image of the page showing that the user is logged in:

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 gets printed in the browser:

Securing Graphcool Endpoint

We only accounted for generating tokens but what happens when a user tries to access a restricted backend route? Of course, an authenticated user will still have access because we haven’t told the server about the token.

We 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('scotch_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, we update it to use the middleware we 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.

Protecting 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 it’s 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 user is not authenticated.

Update App.js to redirect to the home page if the user goes to profile but 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;

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

Conclusion

We just learned how to authenticate a user using Auth0 in a GraphQL project. What you can do is go to your Auth0 dashboard and add few more social authentication options like Twitter. You will be asked for your Twitter developer credentials which can get from the Twitter Developer website.