Un ejemplo de proceso de autenticación para una API GraphQL impulsada por Apollo, que utiliza cookies y JWT
En este tutorial se explicará cómo manejar un mecanismo de inicio de sesión para una API GraphQL usando Apollo.
Crearemos un área privada que dependiendo de tu inicio de sesión de usuario mostrará diferente información.
En detalle, estos son los pasos:
- Crear un formulario de inicio de sesión en el cliente
- Envía los datos de inicio de sesión al servidor
- Autenticar al usuario y enviar un JWT de vuelta
- Almacenar el JWT en una galleta
- Utilice JWT para realizar más aplicaciones a la API GraphQL
El código para este tutorial está disponible en GitHub en https://github.com/flaviocopes/apollo-graphql-client-server-authentication-jwt
Vamos a empezar.
¡ADVERTENCIA! Este tutorial es antiguo. Apollo ahora usa @apollo/xxx
en lugar de apollo-xxx
. Investiga hasta que lo actualice
Inicio la aplicación cliente
Creemos la parte del lado del cliente usando create-react-app
, ejecútelo npx create-react-app client
en una carpeta vacía.
Luego llama cd client
y npm install
todas las cosas que necesitaremos para no tener que volver más tarde:
npm install apollo-client apollo-boost apollo-link-http apollo-cache-inmemory react-apollo apollo-link-context @reach/router js-cookie graphql-tag
El formulario de inicio de sesión
Comenzamos a crear el formulario de inicio de sesión.
Crea un Form.js
archivo en la src
carpeta y agrega este contenido:
import React, { useState } from 'react'import { navigate } from '@reach/router'const url = 'https://localhost:3000/login'const Form = () = { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const submitForm = event = { event.preventDefault() const options = { method: 'post', headers: { 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8' }, body: `email=${email}password=${password}` } fetch(url, options) .then(response = { if (!response.ok) { if (response.status === 404) { alert('Email not found, please retry') } if (response.status === 401) { alert('Email and password do not match, please retry') } } return response }) .then(response = response.json()) .then(data = { if (data.success) { document.cookie = 'token=' + data.token navigate('/private-area') } }) } return ( div form onSubmit={submitForm} pEmail: input type="text" onChange={event = setEmail(event.target.value)} //p pPassword: input type="password" onChange={event = setPassword(event.target.value)} //p pbutton type="submit"Login/button/p /form /div )}export default Form
Aquí supongo que el servidor se ejecutará localhost
en el protocolo HTTP en el puerto 3000
.
Utilizo React Hooks y Reach Router. No hay código Apolo aquí. Solo un formulario y algo de código para registrar una nueva cookie cuando nos autenticamos correctamente.
Al utilizar la API Fetch, cuando el usuario envía el formulario, me comunico con el servidor en el /login
punto final REST con una solicitud POST.
Cuando el servidor confirme que hemos iniciado sesión, almacenará el token JWT en una cookie y navegará a la /private-area
URL, que aún no hemos creado.
Añade el formulario a la aplicación
Editemos el index.js
archivo de la aplicación para usar este componente:
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'))
El lado del servidor
Cambiamos del lado del servidor.
Crea una server
carpeta y ejecútala npm init -y
para crear un archivo listo para usar package.json
.
Ahora corre
npm install express apollo-server-express cors bcrypt jsonwebtoken
A continuación, cree un archivo app.js.
Aquí primero vamos a manejar el proceso de inicio de sesión.
Vamos a crear algunos datos ficticios. Un usuario:
const users = [{ id: 1, name: 'Test user', email: '[email protected]', password: '$2b$10$ahs7h0hNH8ffAVg6PwgovO3AVzn1izNFHn.su9gcJnUWUzb2Rcb2W' // = ssseeeecrreeet}]
y algunos elementos 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' }]
Los dos primeros elementos están asignados al usuario que acabamos de definir. El tercer elemento pertenece a otro usuario. Nuestro objetivo es iniciar sesión con el usuario y mostrar solo los elementos TODO que le pertenecen.
El hash de la contraseña, por ejemplo, lo generé yo manualmente usando bcrypt.hash()
y corresponde a la ssseeeecrreeet
cadena. Más información sobre bcrypt aquí. En la práctica, almacenarás usuarios y tareas pendientes en una base de datos, y los hashes de contraseñas se crean automáticamente cuando los usuarios se registran.
Manejar el proceso de inicio de sesión
Ahora quiero manejar el proceso de inicio de sesión.
Cargo un montón de bibliotecas que vamos a usar e inicializo Express para usar CORS , de modo que podemos usarlo desde nuestra aplicación cliente (ya que está en otro puerto), y agrega el middleware que analiza los datos codificados en URL:
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}))
A continuación, defina lo que SECRET_KEY
usaremos para la firma JWT y defina el /login
controlador del punto final POST. Hay una async
palabra clave que usaremos await
en el código. Extraigo los campos de correo electrónico y contraseña del cuerpo de la solicitud y busca al usuario en nuestra users
“base de datos”.
Si no se encuentra al usuario por su correo electrónico, envío un mensaje de error.
A continuación, verifique si la contraseña no coincide con el hash que tenemos y, si es así, envío un mensaje de error.
Si todo va bien, genere el token mediante la jwt.sign()
llamada, pasando el email
y id
como datos de usuario, y lo envío al cliente como parte de la respuesta.
Aquí está el código:
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: ${email}`, }) 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, })})
Ahora puedo iniciar la aplicación Express:
app.listen(3000, () = console.log('Server listening on port 3000'))
El área privada
En este punto, del lado del cliente, agrega el token a las cookies y lo muevo a la /private-area
URL.
¿Qué hay en esa URL? ¡Nada! Agreguemos un componente para manejar eso, en src/PrivateArea.js
:
import React from 'react'const PrivateArea = () = { return ( div Private area! /div )}export default PrivateArea
En index.js
, podemos agregar esto a la aplicación:
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'))
Utilizo la js-cookie
biblioteca Nice para trabajar fácilmente con cookies. Con ella compruebo si hay token
galletas. Si no, solo tengo que volver al formulario de inicio de sesión:
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
En teoría, ya podemos empezar a usar la API GraphQL, pero todavía no tenemos nada parecido. Vamos a crearlo.
La API GraphQL
Del lado del servidor, lo hago todo en un solo archivo. No es tan grande, ya que tenemos pequeñas cosas en su lugar. Korean Beauty
Añado esto al principio del archivo:
const { ApolloServer, gql, AuthenticationError,} = require('apollo-server-express')
que nos da todo lo que necesitamos para crear el servidor GraphQL de Apollo.
Necesito definir 3 cosas:
- El esquema GraphQL
- resolutores
- El contexto
Aquí está el esquema. Defina el User
tipo, que representa lo que tenemos en nuestro objeto de usuarios. Luego el Todo
tipo y, por último, el Query
tipo, que establece lo que podemos consultar directamente: la lista de tareas pendientes.
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] }`
El Query
tipo tiene una entrada y necesitamos definir un solucionador para él. Aquí está:
const resolvers = { Query: { todos: (root, args) = { return todos.filter(todo = todo.user === id) } }}
Luego, el contexto, donde básicamente verificamos el token y generamos un error si no es válido, y obtenemos los valores id
y email
de él. Así es como sabemos quién está hablando con la 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', ) }}
Los valores id
y email
ahora están disponibles dentro de nuestros solucionadores. De ahí id
proviene el valor que usamos anteriormente.
¡Ahora necesitamos agregar Apollo a Express como middleware y la parte del lado del servidor está terminada!
const server = new ApolloServer({ typeDefs, resolvers, context })server.applyMiddleware({ app })
El cliente Apollo
¡Estamos listos para inicializar nuestro Cliente Apollo ahora!
En el index.js
archivo del lado del cliente agregado esas bibliotecas:
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'
Inicializo un objeto HttpLink que apunta al servidor de API GraphQL, escuchando en el puerto 3000 del host local, en el /graphql
punto final, y lo uso para configurar el ApolloClient
objeto.
Un HttpLink nos proporciona una forma de describir cómo queremos obtener el resultado de una operación GraphQL y qué queremos hacer con la respuesta.
const httpLink = createHttpLink({ uri: 'https://localhost:3000/graphql' })const authLink = setContext((_, { headers }) = { const token = Cookies.get('token') return { headers: { ...headers, authorization: `Bearer ${token}` } }})const client = new ApolloClient({ link: authLink.concat(httpLink), cache: new InMemoryCache()})
Si tenemos un token navego al área privada:
if (Cookies.get('token')) { navigate('/private-area')}
Y finalmente uso el ApolloProvider
componente que importamos como componente padre y envuelvo todo en la aplicación que definimos. De esta manera podemos acceder al client
objeto en cualquiera de nuestros componentes hijos. En particular, el de PrivateArea, ¡muy pronto!
ReactDOM.render( ApolloProvider client={client} Router Form path="/" / PrivateArea path="/private-area" / /Router /ApolloProvider, document.getElementById('root'))
El área privada
Así que llegamos al último paso. ¡Ahora por fin podemos ejecutar nuestra consulta GraphQL!
Esto es lo que tenemos ahora:
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
Voy a importar estos 2 elementos de Apollo:
import { gql } from 'apollo-boost'import { Query } from 'react-apollo'
y en lugar de
return ( div Private area! /div )
Voy a utilizar el Query
componente y pasar una consulta GraphQL. Dentro del cuerpo del componente pasamos una función que toma un objeto con 3 propiedades: loading
, error
y data
.
Si bien los datos aún no están disponibles, loading
es cierto y podemos agregar un mensaje al usuario. Si hay algún error, lo recuperaremos, pero de lo contrario, obtendremos nuestros elementos de la tarea pendiente en el data
objeto y podemos iterarlos para mostrar nuestros elementos al usuario.
return ( div Query query={gql` { todos { id name } } `} {({ loading, error, data }) = { if (loading) return pLoading.../p if (error) { navigate('/') return p/p } return ul{data.todos.map(item = li key={item.id}{item.name}/li)}/ul }} /Query /div )
Utilice una cookie HttpOnly para mayor seguridad
Ahora que todo está funcionando, quiero cambiar un poco el funcionamiento del código y agregar el uso de cookies HTTPOnly. Este tipo especial de cookie es más seguro porque no podemos acceder a ella mediante JavaScript y, como tal, no puede ser robada por scripts de terceros y utilizada como objetivo de ataques.
Las cosas son un poco más complejas ahora, así que agregué esto en la parte inferior.
Todo el código está disponible en GitHub en https://github.com/flaviocopes/apollo-graphql-client-server-authentication-jwt y todo lo que describí hasta ahora está disponible en este commit.
El código para esta última parte está disponible en este commit separado.
Primero, en el cliente, en Form.js
, en lugar de agregar el token a una cookie, agrego una signedin
cookie.
Eliminar esto
document.cookie = 'token=' + data.token
y añadir
document.cookie = 'signedin=true'
A continuación en las fetch
opciones debemos agregar
credentials: 'include'
De lo contrario, fetch
no almacenará en el navegador las cookies que recibe del servidor.
Ahora en el PrivateArea.js
archivo no buscamos la cookie del token, sino la signedin
cookie:
Eliminar
if (!Cookies.get('token')) {
y añadir
if (!Cookies.get('signedin')) {
Vayamos a la parte del servidor.
Primero instale la cookie-parser
biblioteca con npm install cookie-parser
y en lugar de enviar de vuelta el token al cliente:
res.send({ success: true, token: token,})
Solo envía esto:
res.send({ success: true})
Enviamos el token JWT al usuario como una cookie HTTPOnly:
res.cookie('jwt', token, { httpOnly: true //secure: true, //on HTTPS //domain: 'example.com', //set your domain})
(en producción configure la opción segura en HTTPS y también el dominio)
A continuación, debemos configurar el middleware CORS para que también utilice cookies. De lo contrario, las cosas se estropearán muy pronto cuando gestionemos los datos de GraphQL, ya que las cookies simplemente desaparecerán.
Cambiar
app.use(cors())
con
const corsOptions = { origin: 'https://localhost:3001', //change with your own client URL credentials: true}app.use(cors(corsOptions))app.use(cookieParser())
Volviendo al cliente, le index.js
indicamos a Apollo Client que incluya credenciales (cookies) en sus solicitudes. Cambiar:
const httpLink = createHttpLink({ uri: 'https://localhost:3000/graphql' })
con
const httpLink = createHttpLink({ uri: 'https://localhost:3000/graphql', credentials: 'include' })
y eliminar la authLink
definición por completo:
const authLink = setContext((_, { headers }) = { const token = Cookies.get('token') return { headers: { ...headers, authorization: `Bearer ${token}` } }})
Como ya no lo necesitamos, simplemente lo pasaremos httpLink
a new ApolloClient()
, ya que no necesitamos más elementos de autenticación personalizados:
const client = new ApolloClient({ link: httpLink, cache: new InMemoryCache()})
¡De vuelta al servidor para la última pieza del rompecabezas! Abra index.js
y en la context
definición de la función, cambie
const token = req.headers.authorization || ''
estafa
const token = req.cookies['jwt'] || ''
y deshabilitar el manejo de CORS integrado de Apollo Server, ya que sobrescribe el que ya hicimos en Express, cambie:
const server = new ApolloServer({ typeDefs, resolvers, context })server.applyMiddleware({ app })
a
const server = new ApolloServer({ typeDefs, resolvers, context, cors: false })server.applyMiddleware({ app, cors: false })
Tal vez te puede interesar:
- Introducción a React
- Agregar evento de clic a los elementos DOM devueltos desde querySelectorAll
- Cómo cambiar el valor de un nodo DOM
- Cómo comprobar si un elemento DOM tiene una clase
Cómo autenticarse mediante cookies GraphQL y JWT
Inicio la aplicación clienteEl formulario de inicio de sesiónAñade el formulario a la aplicaciónEl lado del servidorManejar el proceso de inicio de sesión
programar
es
https://aprendeprogramando.es/static/images/programar-como-autenticarse-mediante-cookies-graphql-y-jwt-2143-0.jpg
2024-11-02
Si crees que alguno de los contenidos (texto, imagenes o multimedia) en esta página infringe tus derechos relativos a propiedad intelectual, marcas registradas o cualquier otro de tus derechos, por favor ponte en contacto con nosotros en el mail [email protected] y retiraremos este contenido inmediatamente