Implementazione dell’autenticazione utente in Express.js utilizzando JWT

GraphQL è un’alternativa popolare alla tradizionale architettura API RESTful, offrendo un linguaggio di query e manipolazione dei dati flessibile ed efficiente per le API. Con la sua crescente adozione, diventa sempre più importante dare priorità alla sicurezza delle API GraphQL per proteggere le applicazioni da accessi non autorizzati e potenziali violazioni dei dati.

Un approccio efficace per proteggere le API GraphQL è l’implementazione di JSON Web Token (JWT). I JWT forniscono un metodo sicuro ed efficiente per garantire l’accesso alle risorse protette ed eseguire azioni autorizzate, garantendo una comunicazione sicura tra client e API.

Autenticazione e autorizzazione nelle API GraphQL

A differenza delle API REST, le API GraphQL hanno in genere un singolo endpoint che consente ai client di richiedere dinamicamente quantità variabili di dati nelle loro query. Sebbene questa flessibilità sia il suo punto di forza, aumenta anche il rischio di potenziali attacchi alla sicurezza come le vulnerabilità del controllo degli accessi interrotti.

Per mitigare questo rischio, è importante implementare robusti processi di autenticazione e autorizzazione, inclusa la corretta definizione delle autorizzazioni di accesso. In questo modo, garantisci che solo gli utenti autorizzati possano accedere alle risorse protette e, in definitiva, riduci il rischio di potenziali violazioni della sicurezza e perdita di dati.

Puoi trovare il codice di questo progetto nel suo file GitHub deposito.

Configura un server Apollo Express.js

Apollo Server è un’implementazione del server GraphQL ampiamente utilizzata per le API GraphQL. Puoi usarlo per creare facilmente schemi GraphQL, definire risolutori e gestire diverse origini dati per le tue API.

Per configurare un Express.js Apollo Server, crea e apri una cartella di progetto:

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

Successivamente, esegui questo comando per inizializzare un nuovo progetto Node.js utilizzando npm, il gestore pacchetti Node:

 npm init --yes 

Ora installa questi pacchetti.

 npm install apollo-server graphql mongoose jsonwebtokens dotenv 

Infine, crea un file server.js nella directory root e configura il tuo server con questo codice:

 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("Connected to DB");
    return server.listen({ port: 5000 });
  })
  .then((res) => {
    console.log(`Server running at ${res.url}`);
  })
  .catch(err => {
    console.log(err.message);
  });

Il server GraphQL è configurato con i parametri typeDefs e risolutori, specificando lo schema e le operazioni che l’API può gestire. L’opzione context configura l’oggetto req nel contesto di ciascun risolutore, che consentirà al server di accedere ai dettagli specifici della richiesta come i valori dell’intestazione.

Crea un database MongoDB

Per stabilire la connessione al database, crea prima un database MongoDB o imposta un cluster su MongoDB Atlas. Quindi, copia la stringa URI di connessione al database fornita, crea un file .env e inserisci la stringa di connessione come segue:

 MONGO_URI="<mongo_connection_uri>"

Definire il modello di dati

Definire un modello di dati utilizzando Mongoose. Crea un nuovo file models/user.js e includi il seguente codice:

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

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

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

Definire lo schema GraphQL

In un’API GraphQL, lo schema definisce la struttura dei dati che possono essere interrogati, oltre a delineare le operazioni disponibili (query e mutazioni) che è possibile eseguire per interagire con i dati tramite l’API.

Per definire uno schema, crea una nuova cartella nella directory principale del tuo progetto e chiamala graphql. All’interno di questa cartella, aggiungi due file: typeDefs.js e risolutori.js.

Nel file typeDefs.js, includi il seguente codice:

 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;

Crea risolutori per l’API GraphQL

Le funzioni del risolutore determinano il modo in cui i dati vengono recuperati in risposta alle query e alle mutazioni del client, nonché ad altri campi definiti nello schema. Quando un client invia una query o una mutazione, il server GraphQL attiva i risolutori corrispondenti per elaborare e restituire i dati richiesti da varie fonti, come database o API.

Per implementare l’autenticazione e l’autorizzazione utilizzando JSON Web Token (JWT), definire i risolutori per le mutazioni di registro e accesso. Questi gestiranno i processi di registrazione e autenticazione dell’utente. Quindi, crea un risolutore di query di recupero dati che sarà accessibile solo agli utenti autenticati e autorizzati.

Ma prima, definisci le funzioni per generare e verificare i JWT. Nel file risolutori.js, inizia aggiungendo le seguenti importazioni.

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

Assicurati di aggiungere la chiave segreta che utilizzerai per firmare i token Web JSON nel file .env.

 SECRET_KEY = '<my_Secret_Key>'; 

Per generare un token di autenticazione, includere la seguente funzione, che specifica anche attributi univoci per il token JWT, ad esempio la data di scadenza. Inoltre, puoi incorporare altri attributi come “emesso in tempo” in base ai requisiti specifici dell’applicazione.

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

  return token;
}

Implementare ora la logica di verifica dei token per convalidare i token JWT inclusi nelle successive richieste HTTP.

 function verifyToken(token) {
  if (!token) {
    throw new Error('Token not provided');
  }

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

Questa funzione prenderà un token come input, ne verificherà la validità utilizzando la chiave segreta specificata e restituirà il token decodificato se è valido, altrimenti genererà un errore che indica un token non valido.

Definire gli API Resolver

Per definire i risolutori per l’API GraphQL, è necessario delineare le operazioni specifiche che gestirà, in questo caso le operazioni di registrazione e accesso dell’utente. Innanzitutto, crea un oggetto risolutore che manterrà le funzioni del risolutore, quindi definisci le seguenti operazioni di mutazione:

 const resolvers = {
  Mutation: {
    register: async (_, { userInput: { name, password, role } }) => {
      if (!name || !password || !role) {
        throw new Error('Name password, and role required');
     }

      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('Failed to create user');
      }
    },
    login: async (_, { name, password }) => {
      try {
        const user = await User.findOne({ name: name });

        if (!user) {
          throw new Error('User not found');
       }

        if (password !== user.password) {
          throw new Error('Incorrect password');
        }

        const token = generateToken(user);

        if (!token) {
          throw new Error('Failed to generate token');
        }

        return {
          message: 'Login successful',
          token: token,
        };
      } catch (error) {
        console.error(error);
        throw new Error('Login failed');
      }
    }
  },

La mutazione del registro gestisce il processo di registrazione aggiungendo i dati del nuovo utente al database. Mentre la mutazione di accesso gestisce gli accessi degli utenti, in caso di autenticazione riuscita, genererà un token JWT, oltre a restituire un messaggio di successo nella risposta.

Ora includi il risolutore di query per il recupero dei dati utente. Per garantire che questa query sia accessibile solo agli utenti autenticati e autorizzati, includere la logica di autorizzazione per limitare l’accesso solo agli utenti con ruolo di amministratore.

In sostanza, la query controllerà prima la validità del token e poi il ruolo dell’utente. Se il controllo di autorizzazione ha esito positivo, la query del risolutore procederà a recuperare e restituire i dati degli utenti dal database.

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

        if (decodedToken.role !== 'Admin') {
          throw new ('Unauthorized. Only Admins can access this data.');
        }

        const users = await User.find({}, { name: 1, _id: 1, role:1 });
        return users;
      } catch (error) {
        console.error(error);
        throw new Error('Failed to fetch users');
      }
    },
  },
};

Infine, avvia il server di sviluppo:

 node server.js 

Eccezionale! Ora vai avanti e testa la funzionalità dell’API utilizzando la sandbox API di Apollo Server nel tuo browser. Ad esempio, è possibile utilizzare la mutazione del registro per aggiungere nuovi dati utente nel database e quindi la mutazione dell’accesso per autenticare l’utente.

Infine, aggiungi il token JWT alla sezione dell’intestazione dell’autorizzazione e procedi a interrogare il database per i dati dell’utente.

Protezione delle API GraphQL

L’autenticazione e l’autorizzazione sono componenti cruciali per proteggere le API GraphQL. Tuttavia, è importante riconoscere che da soli potrebbero non essere sufficienti a garantire una sicurezza globale. Dovresti implementare misure di sicurezza aggiuntive come la convalida dell’input e la crittografia dei dati sensibili.

Adottando un approccio alla sicurezza completo, puoi salvaguardare le tue API da diversi potenziali attacchi.