This tutorial is out of date and no longer maintained.
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:
We will also learn how to protect React routes from being accessed if the user fails to be authenticated by our authentication server.
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:
- npm install -g create-react-app graphcool-framework
Then use the React CLI tool to scaffold a new React app:
- create-react-app do-auth
Create the Graphcool server by moving into the React app you just created and running the Graphcool init command:
- cd do-auth
- graphcool-framework init server
You need both public and protected routes:
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:
- 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
- graphcool-framework deploy
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:
- 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:
- graphcool-framework
If it’s your first time using Graphcool, you should be taken to a page to create a Graphcool account.
Your 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.
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:
- 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:
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.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.
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:
- 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:
- 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.
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.
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.
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.
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.
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.
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.
Join our DigitalOcean community of over a million developers for free! Get help and share knowledge in our Questions & Answers section, find tutorials and tools that will help you grow as a developer and scale your project or business, and subscribe to topics of interest.
Sign up nowThis 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!