Juan David Gonzalez Revelo
Jose Nuñez
Juan Pablo Cortés
@shimozurdo, gracias crack!
¿Cómo implementar autenticación utilizando JWT en GraphQL?
GraphQL se ha convertido en la herramienta de gestión de API preferida por muchos desarrolladores debido a su flexibilidad y eficiencia. Cuando se trata de seguridad, una práctica común es el uso de JSON Web Tokens (JWT) para autenticación. Aquí exploraremos cómo implementar un sistema de autenticación con JWT en un entorno GraphQL, garantizando así un acceso seguro a las rutas y funcionalidades de nuestra API.
¿Cómo migrar la autenticación de REST API a GraphQL?
En una arquitectura tradicional de REST API, la autenticación se maneja mediante solicitudes a un endpoint específico, enviando usuario y contraseña, y recibiendo a cambio un token de acceso. Esta lógica se puede trasladar a GraphQL mediante la definición de mutaciones y el uso de resolvers.
Definición del esquema en GraphQL:
En GraphQL, el primer paso es crear una mutación específica para manejar el "login". No se requiere un input complejo si simplemente recibimos email y contraseña como parámetros. El esquema debe crear un "out response" con el token de acceso y la información del usuario.
type Mutation { login(email: String!, password: String!): AuthResponse } type AuthResponse { accessToken: String user: User } type User { id: ID! email: String! role: String createdAt: String }
Creación del Resolver para el Login:
El resolver es el encargado de procesar las credenciales y generar el token de acceso. Inicialmente, se define una función con los argumentos necesarios y un retorno nulo.
const login = async (_, { email, password }) => { // Lógica para autenticar al usuario y generar token return null; // Se cambia posteriormente cuando se implemente el JWT }
¿Cómo utilizar Passport para la autenticación?
Utilizar la biblioteca passport y su variante graphql-passport facilita la implementación de estrategias de autenticación con GraphQL de manera similar a REST.
Instalación y configuración de GraphQL Passport:
Primero, instalamos la biblioteca necesaria:
npm install graphql-passport
Definición de estrategia en GraphQL:
Creamos una nueva estrategia que se ajuste a las especificaciones de GraphQL.
const { GraphQLLocalStrategy } = require('graphql-passport'); const localGraphQLStrategy = new GraphQLLocalStrategy((email, password, done) => { // Lógica de autenticación done(null, user); });
Configuración del servidor GraphQL:
Es fundamental adicionar un contexto que permita el uso de Passport en el manejo de peticiones dentro de GraphQL.
const { buildContext } = require('graphql-passport'); const { ApolloServer } = require('apollo-server'); const server = new ApolloServer({ schema, context: ({ req, res }) => buildContext({ req, res }), });
¿Cómo validar credenciales y generar tokens?
Uso del servicio de autenticación:
Implementar un servicio de autenticación que se encargue de verificar las credenciales y generar el JWT es el siguiente paso crítico.
const authenticateUser = async (email, password) => { // Lógica para verificar credenciales y generar token return { user, token }; };
Manejo de errores y respuestas desde el Resolver:
El manejo adecuado de errores permite responder correctamente a intentos de login fallidos, devolviendo códigos de error significativos.
if (!user) { throw new AuthenticationError('Invalid credentials'); }
Finalmente, al migrar la autenticación de un entorno REST a GraphQL, mantenemos la seguridad y control sobre las rutas de acceso a través del uso estratégico de JWT y Passport. El resultado es una API más segura y eficiente que facilita el control de sesiones y permisos en la aplicación.
¿Cómo se realiza el testing del sistema de autenticación?
A través del "Playground" de GraphQL, podemos realizar pruebas de las mutaciones de login enviando datos específicos de usuario y verificando las respuestas en las distintas situaciones: credenciales correctas, incorrectas y manejos esperados de token.
En mi caso el apollo server que está usando el profesor ya está deprecated entonces tuve que pasar a la versión de Apollo Server 4 y mi configuración para iniciar el servidor del mismo quedo de esta manera:
const { ApolloServer } = require('@apollo/server'); const { expressMiddleware } = require('@apollo/server/express4'); const { ApolloServerPluginLandingPageLocalDefault } = require('@apollo/server/plugin/landingPage/default') const { loadFiles } = require('@graphql-tools/load-files') const { buildContext } = require('graphql-passport') const resolvers = require('./resolvers') //crear servidor de GraphQL const useGraphql = async (app) => { const server = new ApolloServer({ typeDefs: await loadFiles('./src/**/*.graphql'), //llamada de schema.graphql resolvers, // llamada de Querys y Mutations plugins: [ // Install a landing page plugin based on NODE_ENV process.env.NODE_ENV === 'production' ? ApolloServerPluginLandingPageLocalDefault({ graphRef: 'API-graphql@graph-variant', footer: false, }) : ApolloServerPluginLandingPageLocalDefault({ footer: false }), ], }); await server.start(); app.use( '/graphql', expressMiddleware(server, { context: async ({req, res}) => buildContext({req, res}) }), ); }; module.exports = useGraphql;
Tal vez lo único que a ustedes les puede variar es el nombre de constantes o variables.
Espero les ayude 💚
Resumen de esta clase acomodamos la consulta con una mutacion
type AuthResponse{ access_token: String! user:User! } type User{ id:ID! email:String! role:String! createdAt:String! } type Mutation { createPerson(name: String! lastname:String! phone:Int): Person! createProduct(dto:CreateProdutDto!):Product updateProduct(id:ID! dto:UpdateProduct!):Product deleteProduct(id:ID!):String # login auth login(email:String! password:String!):AuthResponse }
creamos el resolver y lo injectamos en nuestros resolvers
const login = (root, {email,password})=>{ return null } module.exports ={login}
resolveerss.js
const { login } = require("./auth/auth.resolver") const { allCategories, categoryById } = require("./categories.resolver") const { product, products ,createProduct,deleteProduct,updateProduct} = require("./product.resolver") const { getPersons , findPerson, createPerson} = require("./resolver.person") const resolvers= { Query:{ saludo: ()=> 'hola mundillo de internet', persons:getPersons, findPerson, allCategories, categoryById, product, products }, Mutation:{ createPerson, createProduct, deleteProduct, updateProduct, login //login } } module.exports = resolvers
instalamos la libreria graphql-passport
npm i graphql-passport
creamos nuestra estrategia de graphql y la implementamos en nuestras estategias
gql-local.strategy.js
const {GraphQLLocalStrategy } = require('graphql-passport') const AuthService = require('../../../services/auth.service') const service = new AuthService(); const GQLLocalStrategy = new GraphQLLocalStrategy(async(email,password,done)=>{ try { const user = await service.getUser(email,password); done(null,user) } catch (error) { done(error, false) } }) module.exports = GQLLocalStrategy
añadimos la estrategy
const passport = require('passport'); const LocalStrategy = require('./strategies/local.strategy'); const JwtStrategy = require('./strategies/jwt.strategy'); const GQLLocalStrategy = require('./strategies/local-gql.strategy'); passport.use(LocalStrategy); passport.use(JwtStrategy); passport.use(GQLLocalStrategy)
le envolvemos nuestra estrategy al momento de motar nuestro server de graphql
const {ApolloServer}= require('apollo-server-express'); const {ApolloServerPluginLandingPageLocalDefault} = require('apollo-server-core'); const {loadFiles} = require('@graphql-tools/load-files') const {buildContext}= require('graphql-passport') const resolvers = require('./resolvers') // el ! significa un campo obligatorio const useGraphql= async (app)=>{ const server = new ApolloServer({ typeDefs: await loadFiles('./src/**/*.graphql'), resolvers, context:({req,res})=> buildContext({req,res}), //envolvemos context de graphql para passport plugins: [ApolloServerPluginLandingPageLocalDefault] }); await server.start(); server.applyMiddleware({app}) } module.exports = useGraphql;
y por ultimo en el resolver del login hacemos la autenticacion
const AuthService = require("../../../services/auth.service") const service = new AuthService();// servicio de autenticacion const login = async(root, {email,password},context)=>{ // recibimos el context de graphql-strategy o graphql-passport const {user}= await context.authenticate('graphql-local',{email,password}) console.log('service.signToken(user): ', service.signToken(user)); return service.signToken(user) //utilizamos el servicio de auth para crear nuestro jwt // y enviar la info del login } module.exports ={login}
Juan David Gonzalez Revelo
Jose Nuñez
Juan Pablo Cortés
@shimozurdo, gracias crack!
Jorge Grullon
Hola, segui todos los pasos del profesor y aparentemente me da este error
{
"errors": [
{
"message": "Cannot return null for non-nullable field AuthResponse.accessToken.",
"locations": [
{
"line": 7,
"column": 5
}
],
"path": [
"login",
"accessToken"
],
alguna idea 🤔
Daniel Corazza
a mi me pasoalgo parecido me decia que el access_Token no podia llegar Null, el token si me generaba pero no me lo asignaba a "access_Token", en el archivo auth.resolver.js en la funcion login lo solucione asi:
const AuthServices = require('./../services/authService'); const service = new AuthServices(); const login = async (_, { email, password }, context) => { const { user } = await context.authenticate('graphql-local', {email, password}); // console.log(user) const { token } = service.signToken(user); return { access_token: token, user }; } module.exports = { login };
Emilio Sala
Irving Juárez
Porque el login tiene que ser con una mutacion si no se modifica nada?
Elkin Daniel Prada Gómez
clasificación de inglees
Hola, aparentemente seguí todos los paso, pero estoy obteniendo este error:
{ "data": { "login": null }, "errors": [ { "message": "context.authenticate is not a function", "locations": [ { "line": 2, "column": 3 } ], "path": [ "login" ], "extensions": { "code": "INTERNAL_SERVER_ERROR", "stacktrace": [ "TypeError: context.authenticate is not a function", " at Object.login (curso-nodejs-graphql\\src\\graphql\\auth.resolvers.js:5:34)",
Alguna idea ? 😬
Yo lo resolvi de este modo con @apollo/server/express4.
const { ApolloServer } = require('@apollo/server'); const { ApolloServerPluginLandingPageLocalDefault, ApolloServerPluginLandingPageProductionDefault, } = require('@apollo/server/plugin/landingPage/default'); const { expressMiddleware } = require('@apollo/server/express4'); const { config } = require('../config'); const { loadFilesSync } = require('@graphql-tools/load-files'); const resolvers = require('./resolvers'); const { buildContext } = require('graphql-passport'); const useGraphQL = async (app) => { const server = new ApolloServer({ typeDefs: loadFilesSync('./**/*.graphql'), resolvers, //context: ({ req, res }) => buildContext({ req, res }), playground: true, plugins: [ !config.dev ? ApolloServerPluginLandingPageProductionDefault({ graphRef: 'superapp@apollo', footer: false, }) : ApolloServerPluginLandingPageLocalDefault({ footer: false }), ], }); await server.start(); app.use( '/graphql', expressMiddleware(server, { context: async ({ req, res }) => buildContext({ req, res }), }) ); }; module.exports = useGraphQL;
en la version 5, usando standaloneServer (en vez de integrarlo con express) quedaria asi:
const { ApolloServer } = require('@apollo/server'); const { startStandaloneServer } = require('@apollo/server/standalone'); const { loadFilesSync } = require('@graphql-tools/load-files'); const passport = require('passport'); const typeDefs = loadFilesSync('./src/**/*.graphql'); const { resolvers } = require('./resolvers'); const server = new ApolloServer({ typeDefs, resolvers }); (async () => { const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, context: async ({ req, res }) => { return { authenticate: (strategy, options) => { return new Promise((resolve, reject) => { passport.authenticate(strategy, options, (err, user, info) => { if (err) reject(err); if (!user) reject(new Error(info?.message || 'Authentication failed')); resolve({ user }); })(req, res); }); }, user: req.user, }; }, }); console.log(`🚀 Servidor GraphQL listo en ${url}`); })();