🚀 Nuova versione beta disponibile! Feedback o problemi? Contattaci

Implementazione di JWT in un'API GraphQL con Apollo: Guida Completa

Codegrind Team•Sep 03 2024

JSON Web Tokens (JWT) è una delle tecniche di autenticazione più popolari per le API moderne, inclusi quelle GraphQL. I JWT consentono di gestire in modo sicuro l’autenticazione e l’autorizzazione degli utenti, grazie alla loro capacità di contenere informazioni su un utente in un formato sicuro e facilmente verificabile.

In questa guida, esploreremo come implementare JWT in un’API GraphQL utilizzando Apollo Server. Vedremo come generare un token JWT al momento del login, come proteggere le query e le mutazioni, e come gestire l’autenticazione nel client.

Prerequisiti

Prima di iniziare, assicurati di avere i seguenti prerequisiti:

  • Node.js installato
  • Conoscenza di base di GraphQL e Apollo Server
  • Un progetto Node.js con Apollo Server giĂ  configurato

Se non hai ancora configurato un progetto con Apollo Server, puoi iniziare creando un nuovo progetto e installando le dipendenze necessarie:

npm init -y
npm install apollo-server graphql jsonwebtoken bcryptjs

Step 1: Configurazione di Base del Server

Installazione delle Dipendenze

Per gestire l’autenticazione con JWT, avrai bisogno delle seguenti librerie:

  • jsonwebtoken: Per creare e verificare i token JWT.
  • bcryptjs: Per criptare e verificare le password degli utenti.

Puoi installarle con il seguente comando:

npm install jsonwebtoken bcryptjs

Creazione di un Server Apollo di Base

Inizia configurando un server Apollo di base con una semplice mutazione per la registrazione e il login degli utenti:

const { ApolloServer, gql } = require("apollo-server");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");

const users = []; // Simulazione di un database di utenti

const SECRET_KEY = "your_secret_key";

const typeDefs = gql`
  type User {
    id: ID!
    username: String!
    token: String
  }

  type Query {
    me: User
  }

  type Mutation {
    register(username: String!, password: String!): User
    login(username: String!, password: String!): User
  }
`;

const resolvers = {
  Query: {
    me: (_, __, context) => {
      // Verifica se l'utente è autenticato
      return context.user;
    },
  },
  Mutation: {
    register: async (_, { username, password }) => {
      const hashedPassword = await bcrypt.hash(password, 10);
      const user = { id: users.length + 1, username, password: hashedPassword };
      users.push(user);
      const token = jwt.sign(
        { id: user.id, username: user.username },
        SECRET_KEY
      );
      return { ...user, token };
    },
    login: async (_, { username, password }) => {
      const user = users.find((user) => user.username === username);
      if (!user) {
        throw new Error("User not found");
      }
      const valid = await bcrypt.compare(password, user.password);
      if (!valid) {
        throw new Error("Incorrect password");
      }
      const token = jwt.sign(
        { id: user.id, username: user.username },
        SECRET_KEY
      );
      return { ...user, token };
    },
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => {
    const token = req.headers.authorization || "";
    if (token) {
      try {
        const user = jwt.verify(token, SECRET_KEY);
        return { user };
      } catch (err) {
        console.error("Invalid token");
      }
    }
  },
});

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

Spiegazione del Codice

  • Registrazione (register): Crea un nuovo utente criptando la password e generando un token JWT.
  • Login (login): Verifica la password dell’utente e, se è corretta, genera un token JWT.
  • Context: Ogni richiesta GraphQL viene eseguita all’interno di un contesto che può contenere informazioni sull’utente autenticato, estratte dal token JWT presente nell’header di autorizzazione.

Step 2: Protezione delle Query e delle Mutazioni

Verifica dell’Autenticazione

Puoi proteggere le tue query e mutazioni assicurandoti che solo gli utenti autenticati possano accedervi. Ecco come potresti modificare la query me per essere accessibile solo agli utenti autenticati:

const resolvers = {
  Query: {
    me: (_, __, context) => {
      if (!context.user) {
        throw new Error("Not authenticated");
      }
      return context.user;
    },
  },
  // ...
};

In questo modo, se l’utente non è autenticato (cioè non è presente nel context), verrà lanciato un errore.

Step 3: Gestione del Client

Inviare il Token JWT dal Client

Nel client, devi assicurarti di inviare il token JWT nelle intestazioni di autorizzazione con ogni richiesta GraphQL. Ecco un esempio utilizzando Apollo Client:

import {
  ApolloClient,
  InMemoryCache,
  ApolloProvider,
  createHttpLink,
} from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

const httpLink = createHttpLink({
  uri: "http://localhost:4000/",
});

const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem("token");
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : "",
    },
  };
});

const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache(),
});

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
);

Esempio di Login e Memorizzazione del Token

Ecco un esempio di come gestire il login e memorizzare il token JWT nel client:

import { useMutation, gql } from "@apollo/client";
import React, { useState } from "react";

const LOGIN_MUTATION = gql`
  mutation login($username: String!, $password: String!) {
    login(username: $username, password: $password) {
      token
    }
  }
`;

function Login() {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [login] = useMutation(LOGIN_MUTATION, {
    onCompleted: (data) => {
      localStorage.setItem("token", data.login.token);
    },
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    login({ variables: { username, password } });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="Username"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
      <input
        type="password"
        placeholder="Password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">Login</button>
    </form>
  );
}

export default Login;

Step 4: Best Practices

  1. Scadenza dei Token: Assicurati che i token JWT abbiano una scadenza (exp) e gestisci correttamente il rinnovo dei token (ad esempio tramite refresh tokens) per migliorare la sicurezza.
  2. Hashing delle Password: Usa un algoritmo di hashing sicuro come bcrypt per criptare le password degli utenti.
  3. Gestione degli Errori: Implementa una gestione robusta degli errori per catturare problemi legati all’autenticazione, come token scaduti o non validi.
  4. Protezione delle Rotte: Proteggi le rotte sensibili nel tuo schema GraphQL richiedendo che l’utente sia autenticato prima di accedervi.