Implementarea autentificării utilizatorilor în Express.js folosind JWT

GraphQL a devenit o opțiune atractivă ca alternativă la arhitectura tradițională API RESTful, oferind un limbaj de interogare și manipulare a datelor extrem de flexibil și eficient. Odată cu creșterea popularității sale, securizarea API-urilor GraphQL devine o prioritate crucială pentru a proteja aplicațiile de accesul neautorizat și de eventualele breșe de securitate.

O metodă eficientă de a securiza API-urile GraphQL este prin utilizarea JSON Web Tokens (JWT). Acestea permit acordarea accesului la resursele protejate și autorizarea acțiunilor într-un mod sigur și eficient, asigurând o comunicare securizată între clienți și API.

Autentificarea și autorizarea în API-urile GraphQL

Spre deosebire de API-urile REST, API-urile GraphQL operează, de obicei, printr-un singur punct final. Acest lucru permite clienților să solicite cantități variabile de date prin intermediul interogărilor. Această flexibilitate, deși un avantaj, expune API-urile riscului unor potențiale atacuri de securitate, cum ar fi vulnerabilitățile legate de controlul accesului defectuos.

Pentru a minimiza riscurile, este imperativ să implementăm mecanisme de autentificare și autorizare robuste. Acestea trebuie să includă o definire precisă a permisiunilor de acces. Astfel, vom asigura că doar utilizatorii autorizați pot accesa resursele protejate, reducând potențialul de încălcări de securitate și pierderi de date.

Codul aferent acestui proiect poate fi consultat pe GitHub.

Configurarea unui server Express.js Apollo

Apollo Server este o implementare populară de server GraphQL pentru API-uri. Acesta facilitează crearea schemelor GraphQL, definirea rezolvărilor și gestionarea diverselor surse de date.

Pentru a configura un server Express.js Apollo, începeți prin a crea și a accesa un folder de proiect:

 mkdir graphql-API-jwt
cd graphql-API-jwt

Apoi, inițializați un nou proiect Node.js cu ajutorul managerului de pachete npm:

 npm init --yes 

Instalați acum pachetele necesare:

 npm install apollo-server graphql mongoose jsonwebtokens dotenv 

În final, creați un fișier server.js în directorul rădăcină și configurați-l după cum urmează:

 const { ApolloServer } = require('apollo-server');
const mongoose = require('mongoose');
require('dotenv').config();

const typeDefs = require("./graphql/typeDefs");
const resolvers = require("./graphql/resolvers");

const server = new ApolloServer({
    typeDefs,
    resolvers,
    context: ({ req }) => ({ req }),
});

const MONGO_URI = process.env.MONGO_URI;

mongoose
  .connect(MONGO_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() => {
    console.log("Conectat la baza de date");
    return server.listen({ port: 5000 });
  })
  .then((res) => {
    console.log(`Serverul rulează la ${res.url}`);
  })
  .catch(err => {
    console.log(err.message);
  });

Serverul GraphQL este configurat cu typeDefs și resolvers, definind schema și operațiunile pe care API-ul le poate executa. Opțiunea context permite serverului să acceseze detalii specifice cererii, cum ar fi valorile din antet, prin obiectul req în contextul fiecărui rezolvator.

Crearea unei baze de date MongoDB

Pentru a stabili conexiunea la baza de date, mai întâi creați o bază de date MongoDB sau un cluster pe MongoDB Atlas. Apoi, copiați șirul URI de conectare, creați un fișier .env și adăugați șirul după cum urmează:

 MONGO_URI="<mongo_connection_uri>"

Definirea modelului de date

Definiți modelul de date folosind Mongoose. Creați un nou fișier model/user.js și includeți următorul cod:

 const {model, Schema} = require('mongoose');

const userSchema = new Schema({
    name: String,
    password: String,
    role: String
});

module.exports = model('user', userSchema);

Definirea schemei GraphQL

Într-un API GraphQL, schema definește structura datelor care pot fi interogate și operațiunile disponibile (interogări și mutații) pentru a interacționa cu datele.

Pentru a defini o schemă, creați un folder graphql în directorul rădăcină și adăugați două fișiere: typeDefs.js și resolvers.js.

În fișierul typeDefs.js, adăugați următorul cod:

 const { gql } = require("apollo-server");

const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    password: String!
    role: String!
  }
  input UserInput {
    name: String!
    password: String!
    role: String!
  }
  type TokenResult {
    message: String
    token: String
  }
  type Query {
    users: [User]
  }
  type Mutation {
    register(userInput: UserInput): User
    login(name: String!, password: String!, role: String!): TokenResult
  }
`;

module.exports = typeDefs;

Crearea rezolvărilor pentru API-ul GraphQL

Funcțiile de rezolvare specifică modul în care sunt preluate datele ca răspuns la interogările și mutațiile clientului, precum și câmpurile definite în schemă. Când un client trimite o interogare sau o mutație, serverul GraphQL activează rezolvările corespunzătoare pentru a procesa și a returna datele necesare din diverse surse, cum ar fi baze de date sau API-uri.

Pentru a implementa autentificarea și autorizarea folosind JSON Web Tokens (JWT), definiți rezolvările pentru mutațiile de înregistrare și conectare. Acestea vor gestiona procesele de înregistrare și autentificare. Apoi, creați un rezolvator de interogare pentru preluarea datelor, care va fi accesibil doar utilizatorilor autentificați și autorizați.

Dar, mai întâi, definiți funcțiile de generare și verificare a JWT-urilor. În fișierul resolvers.js, începeți prin adăugarea următoarelor importuri:

 const User = require("../models/user");
const jwt = require('jsonwebtoken');
const secretKey = process.env.SECRET_KEY;

Asigurați-vă că adăugați cheia secretă pe care o veți folosi pentru a semna jetoanele web JSON în fișierul .env.

 SECRET_KEY = '<my_Secret_Key>'; 

Pentru a genera un jeton de autentificare, includeți următoarea funcție, care specifică și atribute unice pentru jetonul JWT, cum ar fi timpul de expirare. Puteți include și alte atribute, cum ar fi cele emise la timp, în funcție de cerințele specifice aplicației:

 function generateToken(user) {
  const token = jwt.sign(
   { id: user.id, role: user.role },
   secretKey,
   { expiresIn: '1h', algorithm: 'HS256' }
 );

  return token;
}

Acum, implementați logica de verificare a jetoanelor pentru a valida jetoanele JWT incluse în cererile HTTP ulterioare:

 function verifyToken(token) {
  if (!token) {
    throw new Error('Jetonul nu a fost furnizat');
  }

  try {
    const decoded = jwt.verify(token, secretKey, { algorithms: ['HS256'] });
    return decoded;
  } catch (err) {
    throw new Error('Jeton invalid');
  }
}

Această funcție primește un jeton ca intrare, îi verifică validitatea folosind cheia secretă specificată și returnează jetonul decodat dacă este valid. Altfel, aruncă o eroare indicând un jeton invalid.

Definirea rezolvărilor API

Pentru a defini rezolvările pentru API-ul GraphQL, trebuie să specificăm operațiunile pe care le va gestiona, în acest caz, operațiunile de înregistrare și autentificare a utilizatorilor. Mai întâi, creați un obiect resolvers care va conține funcțiile de rezolvare, apoi definiți următoarele operații de mutație:

 const resolvers = {
  Mutation: {
    register: async (_, { userInput: { name, password, role } }) => {
      if (!name || !password || !role) {
        throw new Error('Numele, parola și rolul sunt obligatorii');
     }

      const newUser = new User({
        name: name,
        password: password,
        role: role,
      });

      try {
        const response = await newUser.save();

        return {
          id: response._id,
          ...response._doc,
        };
      } catch (error) {
        console.error(error);
        throw new Error('Eșec la crearea utilizatorului');
      }
    },
    login: async (_, { name, password }) => {
      try {
        const user = await User.findOne({ name: name });

        if (!user) {
          throw new Error('Utilizatorul nu a fost găsit');
       }

        if (password !== user.password) {
          throw new Error('Parolă incorectă');
        }

        const token = generateToken(user);

        if (!token) {
          throw new Error('Eșec la generarea jetonului');
        }

        return {
          message: 'Autentificare reușită',
          token: token,
        };
      } catch (error) {
        console.error(error);
        throw new Error('Autentificare eșuată');
      }
    }
  },

Mutația de înregistrare gestionează adăugarea noilor date de utilizator în baza de date. Mutația de conectare gestionează autentificarea, și, dacă reușește, va genera un jeton JWT și va returna un mesaj de succes.

Acum, adăugați rezolvarea interogării pentru preluarea datelor utilizatorului. Pentru a ne asigura că această interogare este accesibilă doar utilizatorilor autentificați și autorizați, vom include o logică de autorizare care restricționează accesul la utilizatorii cu rol de administrator.

În esență, interogarea va verifica mai întâi validitatea jetonului, apoi rolul utilizatorului. Dacă autorizarea are succes, interogarea va prelua și va returna datele utilizatorilor din baza de date.

   Query: {
    users: async (parent, args, context) => {
      try {
        const token = context.req.headers.authorization || '';
        const decodedToken = verifyToken(token);

        if (decodedToken.role !== 'Admin') {
          throw new ('Neautorizat. Doar administratorii pot accesa aceste date.');
        }

        const users = await User.find({}, { name: 1, _id: 1, role:1 });
        return users;
      } catch (error) {
        console.error(error);
        throw new Error('Eșec la preluarea utilizatorilor');
      }
    },
  },
};

În cele din urmă, porniți serverul de dezvoltare:

 node server.js 

Excelent! Acum, puteți testa funcționalitatea API-ului folosind sandbox-ul Apollo Server API din browser. De exemplu, folosiți mutația de înregistrare pentru a adăuga noi date în baza de date, iar apoi mutația de conectare pentru a autentifica utilizatorul.

Adăugați jetonul JWT în secțiunea de antet Autorizare și apoi interogați baza de date pentru datele utilizatorului.

Securizarea API-urilor GraphQL

Autentificarea și autorizarea sunt esențiale pentru securizarea API-urilor GraphQL. Cu toate acestea, ele singure nu garantează o securitate completă. Este important să implementăm măsuri de securitate suplimentare, cum ar fi validarea intrărilor și criptarea datelor sensibile.

Adoptând o abordare cuprinzătoare, putem proteja API-urile împotriva unei varietăți de atacuri potențiale.