Un exemple de processus d'authentification pour une API GraphQL alimentée par Apollo, utilisant des cookies et JWT
Dans ce tutoriel, je vais vous expliquer comment gérer un mécanisme de connexion pour unGraphQLAPI utilisantApollon.
Nous allons créer une zone privée qui, en fonction de votre connexion utilisateur, affichera des informations différentes.
En détail, voici les étapes:
- Créez un formulaire de connexion sur le client
- Envoyez les données de connexion au serveur
- Authentifiez l'utilisateur et renvoyez un JWT
- Stocker leJWTdans un cookie
- Utilisez le JWT pour d'autres requêtes à l'API GraphQL
Le code de ce tutoriel est disponible sur GitHub à l'adressehttps://github.com/flaviocopes/apollo-graphql-client-server-authentication-jwt
Commençons.
Je lance l'application client
Créons la partie côté client en utilisantcreate-react-app
, Coursnpx create-react-app client
dans un dossier vide.
Puis appelezcd client
etnpm install
tout ce dont nous aurons besoin pour ne pas avoir besoin de revenir plus tard:
npm install apollo-client apollo-boost apollo-link-http apollo-cache-inmemory react-apollo apollo-link-context @reach/router js-cookie graphql-tag
Le formulaire de connexion
Commençons par créer le formulaire de connexion.
Créer unForm.js
fichier dans lesrc
dossier, et ajoutez-y ce contenu:
import React, { useState } from 'react'
import { navigate } from '@reach/router'
const url = ‘http://localhost:3000/login’
const Form = () => {
const [email, setEmail] = useState(’’)
const [password, setPassword] = useState(’’)
const submitForm = event => {
event.preventDefault()
<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">options</span> <span style="color:#f92672">=</span> {
<span style="color:#a6e22e">method</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'post'</span>,
<span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> {
<span style="color:#e6db74">'Content-type'</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'application/x-www-form-urlencoded; charset=UTF-8'</span>
},
<span style="color:#a6e22e">body</span><span style="color:#f92672">:</span> <span style="color:#e6db74">`email=</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">email</span><span style="color:#e6db74">}</span><span style="color:#e6db74">&password=</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">password</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>
}
<span style="color:#a6e22e">fetch</span>(<span style="color:#a6e22e">url</span>, <span style="color:#a6e22e">options</span>)
.<span style="color:#a6e22e">then</span>(<span style="color:#a6e22e">response</span> => {
<span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">ok</span>) {
<span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">status</span> <span style="color:#f92672">===</span> <span style="color:#ae81ff">404</span>) {
<span style="color:#a6e22e">alert</span>(<span style="color:#e6db74">'Email not found, please retry'</span>)
}
<span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">status</span> <span style="color:#f92672">===</span> <span style="color:#ae81ff">401</span>) {
<span style="color:#a6e22e">alert</span>(<span style="color:#e6db74">'Email and password do not match, please retry'</span>)
}
}
<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">response</span>
})
.<span style="color:#a6e22e">then</span>(<span style="color:#a6e22e">response</span> => <span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">json</span>())
.<span style="color:#a6e22e">then</span>(<span style="color:#a6e22e">data</span> => {
<span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">success</span>) {
document.<span style="color:#a6e22e">cookie</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">'token='</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">token</span>
<span style="color:#a6e22e">navigate</span>(<span style="color:#e6db74">'/private-area'</span>)
}
})
}
return (
<div>
<form onSubmit={submitForm}>
<p>Email: <input type=“text” onChange={event => setEmail(event.target.value)} /></p>
<p>Password: <input type=“password” onChange={event => setPassword(event.target.value)} /></p>
<p><button type=“submit”>Login</button></p>
</form>
</div>
)
}
export default Form
Ici, je suppose que le serveur fonctionnera surlocalhost
, sur le protocole HTTP, sur le port3000
.
J'utilise React Hooks, et leAtteindre le routeur. Il n'y a pas de code Apollo ici. Juste un formulaire et du code pour enregistrer un nouveau cookie lorsque nous nous authentifions avec succès.
En utilisant l'API Fetch, lorsque le formulaire est envoyé par l'utilisateur, je contacte le serveur sur le/login
Point de terminaison REST avec une requête POST.
Lorsque le serveur confirmera que nous sommes connectés, il stockera le jeton JWT dans un cookie, et il naviguera vers le/private-area
URL, que nous n'avons pas encore créée.
Ajouter le formulaire à l'application
Modifions leindex.js
fichier de l'application pour utiliser ce composant:
import React from 'react'
import ReactDOM from 'react-dom'
import { Router } from '@reach/router'
import Form from './Form'
ReactDOM.render(
<Router>
<Form path="/" />
</Router>
document.getElementById(‘root’)
)
Le côté serveur
Changeons côté serveur.
Créer unserver
dossier et exécuteznpm init -y
pour créer un prêt à l'emploipackage.json
déposer.
Maintenant, cours
npm install express apollo-server-express cors bcrypt jsonwebtoken
Ensuite, créez un fichier app.js.
Ici, nous allons d'abord gérer le processus de connexion.
Créons des données factices. Un utilisateur:
const users = [{
id: 1,
name: 'Test user',
email: '[email protected]',
password: '$2b$10$ahs7h0hNH8ffAVg6PwgovO3AVzn1izNFHn.su9gcJnUWUzb2Rcb2W' // = ssseeeecrreeet
}]
et quelques articles TODO:
const todos = [
{
id: 1,
user: 1,
name: 'Do something'
},
{
id: 2,
user: 1,
name: 'Do something else'
},
{
id: 3,
user: 2,
name: 'Remember the milk'
}
]
Les 2 premiers d'entre eux sont attribués à l'utilisateur que nous venons de définir. Le troisième élément appartient à un autre utilisateur. Notre objectif est de se connecter à l'utilisateur et d'afficher uniquement les éléments TODO qui lui appartiennent.
Le hachage du mot de passe, pour les besoins de l'exemple, a été généré par moi manuellement en utilisantbcrypt.hash()
et correspond aussseeeecrreeet
chaîne de caractères. Plus d'informations sur bcryptici. En pratique, vous stockerez les utilisateurs et les todos dans une base de données, et les hachages de mots de passe sont créés automatiquement lors de l'inscription des utilisateurs.
Gérez le processus de connexion
Maintenant, je veux gérer le processus de connexion.
Je charge un tas de bibliothèques que nous allons utiliser et j'initialise Express pour l'utiliserCORS, afin que nous puissions l'utiliser à partir de notre application client (comme c'est sur un autre port), et j'ajoute le middleware qui analyse les données urlencodées:
const express = require('express')
const cors = require('cors')
const bcrypt = require('bcrypt')
const jwt = require('jsonwebtoken')
const app = express()
app.use(cors())
app.use(express.urlencoded({
extended: true
}))
Ensuite, je définis unSECRET_KEY
nous allons utiliser pour la signature JWT, et je définis le/login
Gestionnaire de point de terminaison POST. Il y a unasync
mot-clé parce que nous allons utiliserawait
dans le code. J'extrais les champs email et mot de passe du corps de la demande et je recherche l'utilisateur dans notreusers
"base de données".
Si l'utilisateur n'est pas trouvé par son e-mail, je renvoie un message d'erreur.
Ensuite, je vérifie si le mot de passe ne correspond pas au hachage que nous avons et je renvoie un message d'erreur si c'est le cas.
Si tout se passe bien, je génère le jeton en utilisant lejwt.sign()
appel, en passant leemail
etid
en tant que données utilisateur, et je l'envoie au client dans le cadre de la réponse.
Voici le code:
const SECRET_KEY = 'secret!'
app.post(’/login’, async (req, res) => {
const { email, password } = req.body
const theUser = users.find(user => user.email === email)
if (!theUser) {
res.status(404).send({
success: false,
message: Could not find account: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">email</span><span style="color:#e6db74">}</span><span style="color:#e6db74">
,
})
return
}
const match = await bcrypt.compare(password, theUser.password)
if (!match) {
//return error to user to let them know the password is incorrect
res.status(401).send({
success: false,
message: ‘Incorrect credentials’,
})
return
}
const token = jwt.sign(
{ email: theUser.email, id: theUser.id },
SECRET_KEY,
)
res.send({
success: true,
token: token,
})
})
Je peux maintenant démarrer l'application Express:
app.listen(3000, () =>
console.log('Server listening on port 3000')
)
L'espace privé
À ce stade, côté client, j'ajoute le jeton aux cookies et passe au/private-area
URL.
Qu'y a-t-il dans cette URL? Rien! Ajoutons un composant pour gérer cela, danssrc/PrivateArea.js
:
import React from 'react'
const PrivateArea = () => {
return (
<div>
Private area!
</div>
)
}
export default PrivateArea
Dansindex.js
, nous pouvons ajouter ceci à l'application:
import React from 'react'
import ReactDOM from 'react-dom'
import { Router } from '@reach/router'
import Form from './Form'
import PrivateArea from './PrivateArea'
ReactDOM.render(
<Router>
<Form path="/" />
<PrivateArea path="/private-area" />
</Router>
document.getElementById(‘root’)
)
J'utilise le gentiljs-cookie
bibliothèque pour travailler facilement avec les cookies. En l'utilisant, je vérifie s'il y a letoken
dans les cookies. Sinon, retournez simplement au formulaire de connexion:
import React from 'react'
import Cookies from 'js-cookie'
import { navigate } from '@reach/router'
const PrivateArea = () => {
if (!Cookies.get(‘token’)) {
navigate(’/’)
}
return (
<div>
Private area!
</div>
)
}
export default PrivateArea
Maintenant, en théorie, nous sommes tous prêts à utiliser l'API GraphQL! Mais nous n'avons encore rien de tel. Faisons cette chose.
L'API GraphQL
Côté serveur, je fais tout dans un seul fichier. Ce n'est pas si gros, car nous avons de petites choses en place.
J'ajoute ceci en haut du fichier:
const {
ApolloServer,
gql,
AuthenticationError,
} = require('apollo-server-express')
ce qui nous donne tout ce dont nous avons besoin pour créer le serveur Apollo GraphQL.
J'ai besoin de définir 3 choses:
- le schéma GraphQL
- résolveurs
- le contexte
Voici le schéma. Je définis leUser
type, qui représente ce que nous avons dans notre objet utilisateurs. Puis leTodo
type, et enfin leQuery
type, qui définit ce que nous pouvons directement interroger: la liste des tâches.
const typeDefs = gql`
type User {
id: ID!
email: String!
name: String!
password: String!
}
type Todo {
id: ID!
user: Int!
name: String!
}
type Query {
todos: [Todo]
}
`
LeQuery
type a une entrée, et nous devons définir un résolveur pour cela. C'est ici:
const resolvers = {
Query: {
todos: (root, args) => {
return todos.filter(todo => todo.user === id)
}
}
}
Ensuite, le contexte, où nous vérifions essentiellement le jeton et l'erreur s'il est invalide, et nous obtenons leid
etemail
valeurs de celui-ci. C'est ainsi que nous savonsquiparle à l'API:
const context = ({ req }) => {
const token = req.headers.authorization || ''
try {
return { id, email } = jwt.verify(token.split(’ ')[1], SECRET_KEY)
} catch (e) {
throw new AuthenticationError(
‘Authentication token is invalid, please log in’,
)
}
}
Leid
etemail
les valeurs sont maintenant disponibles dans notre (nos) résolveur (s). C'est là que leid
la valeur que nous utilisons ci-dessus vient de.
Nous devons maintenant ajouter Apollo à Express en tant que middleware, et la partie côté serveur est terminée!
const server = new ApolloServer({ typeDefs, resolvers, context })
server.applyMiddleware({ app })
Le client Apollo
Nous sommes prêts à initialiser notre client Apollo maintenant!
Du côté clientindex.js
fichier j'ajoute ces bibliothèques:
import { ApolloClient } from 'apollo-client'
import { createHttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { ApolloProvider } from 'react-apollo'
import { setContext } from 'apollo-link-context'
import { navigate } from '@reach/router'
import Cookies from 'js-cookie'
import gql from 'graphql-tag'
J'initialise un objet HttpLink qui pointe vers le serveur API GraphQL, en écoute sur le port 3000 de localhost, sur le/graphql
endpoint, et utilisez-le pour configurer leApolloClient
objet.
Un HttpLink nous fournit un moyen de décrire comment nous voulons obtenir le résultat d'une opération GraphQL, et ce que nous voulons faire avec la réponse.
const httpLink = createHttpLink({ uri: 'http://localhost:3000/graphql' })
const authLink = setContext((_, { headers }) => {
const token = Cookies.get(‘token’)
return {
headers: {
…headers,
authorization: Bearer </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">token</span><span style="color:#e6db74">}</span><span style="color:#e6db74">
}
}
})
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache()
})
Si nous avons un jeton, je navigue vers la zone privée:
if (Cookies.get('token')) {
navigate('/private-area')
}
et enfin j'utilise leApolloProvider
composant que nous avons importé en tant que composant parent et enveloppez tout dans l'application que nous avons définie. De cette façon, nous pouvons accéder auclient
objet dans l'un de nos composants enfants. En particulier celui de PrivateArea, très bientôt!
ReactDOM.render(
<ApolloProvider client={client}>
<Router>
<Form path="/" />
<PrivateArea path="/private-area" />
</Router>
</ApolloProvider>,
document.getElementById('root')
)
L'espace privé
Nous sommes donc à la dernière étape. Maintenant, nous pouvons enfin effectuer notre requête GraphQL!
Voici ce que nous avons maintenant:
import React from 'react'
import Cookies from 'js-cookie'
import { navigate } from '@reach/router'
const PrivateArea = () => {
if (!Cookies.get(‘token’)) {
navigate(’/’)
}
return (
<div>
Private area!
</div>
)
}
export default PrivateArea
Je vais importer ces 2 éléments depuis Apollo:
import { gql } from 'apollo-boost'
import { Query } from 'react-apollo'and instead of
return (
<div>
Private area!
</div>
)
I’m going to use the Query
component and pass a GraphQL query. Inside the component body we pass a function that takes an object with 3 properties: loading
, error
and data
.
While the data is not available yet, loading
is true and we can add a message to the user. If there’s any error we’ll get it back, but otherwise we’ll get our TO-DO items in the data
object and we can iterate over them to render our items to the user!
return (
<div>
<Query
query={gql`
{
todos {
id
name
}
}
`}
>
{({ loading, error, data }) => {
if (loading) return <p>Loading...</p>
if (error) {
navigate('/')
return <p></p>
}
return <ul>{data.todos.map(item => <li key={item.id}>{item.name}</li>)}</ul>
}}
</Query>
</div>
)
Use an HttpOnly cookie for better security
Now that things are working, I want to change a little bit how the code works and add the use of HTTPOnly cookies. This special kind of cookie is more secure because we can’t access it using JavaScript, and as such it can’t be stolen by 3rd part scripts and used as a target for attacks.
Things are a bit more complex now so I added this at the bottom.
All the code is available on GitHub at https://github.com/flaviocopes/apollo-graphql-client-server-authentication-jwt and all I described up to now is available in this commit.
The code for this last part is available in this separate commit.
First, in the client, in Form.js
, instead of adding the token to a cookie, I add a signedin
cookie.
Remove this
document.cookie = 'token=' + data.token
and add
document.cookie = 'signedin=true'
Next, in the fetch
options we must add
credentials: 'include'
otherwise fetch
won’t store in the browser the cookies it gets back from the server.
Now in the PrivateArea.js
file we don’t check for the token cookie, but for the signedin
cookie:
Remove
if (!Cookies.get('token')) {
and add
if (!Cookies.get('signedin')) {
Let’s go to the server part.
First install the cookie-parser
library with npm install cookie-parser
and instead of sending back the token to the client:
res.send({
success: true,
token: token,
})
Only send this:
res.send({
success: true
})
We send the JWT token to the user as an HTTPOnly cookie:
res.cookie('jwt', token, {
httpOnly: true
//secure: true, //on HTTPS
//domain: 'example.com', //set your domain
})
(in production set the secure option on HTTPS and also the domain)
Next we need to set the CORS middleware to use cookies, too. Otherwise things will break very soon when we manage the GraphQL data, as cookies just disappear.
Change
app.use(cors())
with
const corsOptions = {
origin: 'http://localhost:3001', //change with your own client URL
credentials: true
}
app.use(cors(corsOptions))
app.use(cookieParser())
Back to the client, in index.js
we tell Apollo Client to include credentials (cookies) in its requests. Switch:
const httpLink = createHttpLink({ uri: 'http://localhost:3000/graphql' })
with
const httpLink = createHttpLink({ uri: 'http://localhost:3000/graphql', credentials: 'include' })
and remove the authLink
definition altogether:
const authLink = setContext((_, { headers }) => {
const token = Cookies.get('token')
return {
headers: {
…headers,
authorization: Bearer </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">token</span><span style="color:#e6db74">}</span><span style="color:#e6db74">
}
}
})
as we don’t need it any more. We’ll just pass httpLink
to new ApolloClient()
, since we don’t need more customized authentication stuff:
const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache()
})
Back to the server for the last piece of the puzzle! Open index.js
and in the context
function definition, change
const token = req.headers.authorization || ''
with
const token = req.cookies['jwt'] || ''
and disable the Apollo Server built-in CORS handling, since it overwrites the one we already did in Express, change:
const server = new ApolloServer({ typeDefs, resolvers, context })
server.applyMiddleware({ app })
to
const server = new ApolloServer({ typeDefs, resolvers, context,
cors: false })
server.applyMiddleware({ app, cors: false })
More graphql tutorials:
- An introduction to GraphQL
- A complete introduction to Apollo, the GraphQL toolkit
- How to create a GraphQL Server with Node.js and Express
- How to authenticate using GraphQL Cookies and JWT
- GraphQL API vs REST API