6 min read
Build an Apollo Graphql user authentication for your React app - part 2
In my last post we built a Graphql API that handles user authentication and authorization. In particular we added a loginUser
query that returns a JWT token. This token can be used to access restricted resources.
In this post I will show what the implementation looks like on Reacts side.
Walking through this guide requires basic React knowledge. We will use React Router for routing, React Hooks for executing Graphql queries and Matieral-UI for the component framework.
Files
Let’s get started with an overview of the project files.
Apollo.js
: Apollo client configurationHeaderLoginButton.js
: Links to login page and handles logoutLogin.js
: Login pageProfile.js
: View for user profilequeries.js
: Exports all Graphql queries and mutationsRoutes.js
: React router componenthooks.js
: Export React hooks
2019-09-18 Edit: Added React hook file
So obviously we are not going to build an React app from scratch. This guide assumes you already have one and intent to add authentication and authorization functionality.
Routes
Routes
is a higher-order component (HOC) that connect our view components with specific routes.
Routes.js
import React from 'react'
import { Route } from 'react-router-dom'
import Home from './Home'
import Login from './Login'
import Profile from './Profile'
const Routes = () => (
<>
<Route exact path='/' component={Home} />
<Route exact path='/login' component={Login} />
<Route exact path='/profile' component={Profile} />
</>
)
export default Routes
So if the user opens or gets redirected to /login
the Login
component will be shown.
Login
So what does the Login
component looks like?
Login.js
import React from 'react'
import { useLazyQuery } from '@apollo/react-hooks'
import { Redirect } from 'react-router'
import Paper from '@material-ui/core/Paper'
import Typography from '@material-ui/core/Typography'
import TextField from '@material-ui/core/TextField'
import Button from '@material-ui/core/Button'
import { LOGIN_USER } from './queries'
import Loading from './Loading'
import Error from './Error'
import { useForm } from './hooks'
const Login = () => {
// Use form state
const { values, handleChange, handleSubmit } = useForm((credentials) => loginUser(), {
email: '',
password: ''
})
// Lazy query for login user method
const [loginUser, { called, loading, data, error }] = useLazyQuery(LOGIN_USER, { variables: values })
// Wait for lazy query
if (called && loading) return <Loading />
// Show error message if lazy query fails
if (error) return <Error message={error.message} />
// Store token if login is successful
if (data) {
window.localStorage.setItem('token', data.loginUser.token)
// Redirect to home page
return <Redirect to='/' />
}
return (
<Paper className={classes.paper}>
<Typography className={classes.title} variant='h3' component='h1'>
Login
</Typography>
<form onSubmit={handleSubmit}
>
<TextField
variant='outlined'
margin='normal'
required
fullWidth
id='email'
name='email'
label='Email Address'
type='email'
value={values.email}
onChange={handleChange}
autoFocus
/>
<TextField
variant='outlined'
margin='normal'
required
fullWidth
id='password'
name='password'
label='Password'
type='password'
value={values.password}
onChange={handleChange}
/>
<Button
type='submit'
fullWidth
variant='contained'
color='primary'
className={classes.submit}
>
Sign in
</Button>
</form>
</Paper>
)
}
export default Login
The component presents a login form. When credentials are submitted a React hook is used to run the lazy Graphql query and retrieve the JWT token. If the login is successful the token is stored in the local storage of the browser.
The form state is managed by a React hook as well. See the hooks.js
for the simple useForm
React hook example that can be reused for other form.
As you can see the query is imported from the queries.js
file. This is externalized because we want to share queries among component.
queries.js
import gql from 'graphql-tag'
const GET_CURRENT_USER = gql`
{
currentUser {
firstname
lastname
}
}
`
const LOGIN_USER = gql`
query loginUser($email: String!, $password: String!) {
loginUser(email: $email, password: $password) {
token
}
}
`
export {
GET_CURRENT_USER,
LOGIN_USER
}
And here is the useForm
React hook.
hooks.js
import { useState } from 'react'
const useForm = (callback, data) => {
const [values, setValues] = useState(data)
const handleChange = (event) => {
event.persist()
setValues(values => ({
...values,
[event.target.name]: event.target.value
}))
}
const handleSubmit = (event, onSubmit) => {
event.preventDefault()
callback(values)
}
return {
handleChange,
handleSubmit,
values
}
}
export { useForm }
2019-09-18 Edit: Added useForm React hook
Apollo
We saw that the token is being stored in the local storage. But how is passed to Apollo server? This is done in the Apollo client.
Apollo.js
import ApolloClient from 'apollo-boost'
import React from 'react'
import PropTypes from 'prop-types'
import { ApolloProvider } from '@apollo/react-hooks'
// Initialize Apollo client
const client = new ApolloClient({
uri: process.env.REACT_APP_APOLLO_URL || 'http://localhost:3000/api',
request: async operation => {
// Get JWT token from local storage
const token = window.localStorage.getItem('token')
// Pass token to headers
operation.setContext({
headers: {
Authorization: token ? `Bearer ${token}` : ''
}
})
}
})
// Define Apollo component
const Apollo = ({ children }) => (
<ApolloProvider client={client}>
{children}
</ApolloProvider>
)
Apollo.propTypes = {
children: PropTypes.object.isRequired
}
export default Apollo
This HOC initializes the Apollo client and connects to the API. For every request it check if a token is available and if so forwards it in the Bearer Authorization header.
Logout
Assuming the user has logged in, we can run queries that requires authorization from every other component.
Profile.js
import React from 'react'
import { useQuery } from '@apollo/react-hooks'
import Paper from '@material-ui/core/Paper'
import Typography from '@material-ui/core/Typography'
import { GET_CURRENT_USER } from './queries'
import Loading from './Loading'
import Error from './Error'
const Profile = () => {
const { loading, error, data } = useQuery(GET_CURRENT_USER)
if (loading) return <Loading />
if (error) return <Error message={error.message} />
return (
<Paper>
<Typography variant='h3' component='h1'>
Profil
</Typography>
<Typography component='p'>
{`${data.currentUser.firstname} ${data.currentUser.lastname}`}
</Typography>
</Paper>
)
}
export default Profile
The getCurrentUser
query requires an authentication. Data is only shown if the user has logged in.
Now we got to the final step. Usually you’ll find a login/logout button on the top right of the nav bar. This button is called HeaderLoginButton
in our case and handles the logout.
HeaderLoginButton.js
import React from 'react'
import { useQuery } from '@apollo/react-hooks'
import { Link } from 'react-router-dom'
import Button from '@material-ui/core/Button'
import { GET_CURRENT_USER } from './queries'
const HeaderLoginButton = () => {
const { client, data } = useQuery(GET_CURRENT_USER)
// Reset Apollo and local store on logout
const logout = () => {
window.localStorage.clear()
client.resetStore()
}
if (data && data.currentUser) {
return (
<Button onClick={logout} color='inherit'>Logout</Button>
)
}
return (
<Link to='/login'>
<Button color='inherit'>Login</Button>
</Link>
)
}
export default HeaderLoginButton
If the user is not logged in the button if clicked will redirect to the login page. Once the user has logged in the button will logout the user if clicked. The logout process simply gets rid of the token in the local storage and resets the Apollo client.
Here you can also observe the power React hooks. We don’t have to pass the Apollo client object down the hierarchy. Simply retrieve it from the useQuery
hook!
That’s it! 🎉
Next
In the next and final post we are going to improve our solution regarding security. Cliff hanger questions: What if the user copies the token and paste it after logout? How can we ensure that a token cannot be used indefinitely 😕?
Additions
As already mentioned in my last post I focus on the modifications you have to make to the app in order get a user authentication. Proper implementation of course requires various other features. So here are some I can think of:
- Password reset component
- User profile page
- Proper error and success notification
- Redirect to error page if user is unauthorized
- User signup process
Edits
2019-09-18: Added useForm React hook.
Categories: JavaScript developmentTags: apollo , graphql , authentication , react , json web token , directive , authorization
Edit this page
Show statistic for this page