Implementowanie uwierzytelniania użytkowników w Express.js przy użyciu tokenów JWT

GraphQL to popularna alternatywa dla tradycyjnej architektury RESTful API, oferująca elastyczny i wydajny język zapytań i manipulacji danymi dla interfejsów API. Wraz z rosnącą popularnością, coraz ważniejsze staje się nadanie priorytetu bezpieczeństwu interfejsów API GraphQL w celu ochrony aplikacji przed nieautoryzowanym dostępem i potencjalnymi naruszeniami danych.

Jednym ze skutecznych podejść do zabezpieczania interfejsów API GraphQL jest wdrożenie tokenów sieciowych JSON (JWT). JWT zapewniają bezpieczną i wydajną metodę udzielania dostępu do chronionych zasobów i wykonywania autoryzowanych działań, zapewniając bezpieczną komunikację pomiędzy klientami a API.

Uwierzytelnianie i autoryzacja w API GraphQL

W przeciwieństwie do interfejsów API REST, interfejsy API GraphQL zazwyczaj mają jeden punkt końcowy, który umożliwia klientom dynamiczne żądanie różnych ilości danych w swoich zapytaniach. Chociaż ta elastyczność jest jego mocną stroną, zwiększa również ryzyko potencjalnych ataków bezpieczeństwa, takich jak luki w zabezpieczeniach kontroli dostępu.

Aby zminimalizować to ryzyko, ważne jest wdrożenie solidnych procesów uwierzytelniania i autoryzacji, w tym odpowiednie zdefiniowanie uprawnień dostępu. W ten sposób gwarantujesz, że tylko autoryzowani użytkownicy będą mieli dostęp do chronionych zasobów, a ostatecznie zmniejszysz ryzyko potencjalnych naruszeń bezpieczeństwa i utraty danych.

Kod tego projektu znajdziesz w jego pliku GitHub magazyn.

Skonfiguruj serwer Apollo Express.js

Serwer Apollo jest szeroko stosowaną implementacją serwera GraphQL dla interfejsów API GraphQL. Można go używać do łatwego tworzenia schematów GraphQL, definiowania funkcji rozpoznawania nazw i zarządzania różnymi źródłami danych dla interfejsów API.

Aby skonfigurować serwer Apollo Express.js, utwórz i otwórz folder projektu:

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

Następnie uruchom to polecenie, aby zainicjować nowy projekt Node.js przy użyciu npm, menedżera pakietów Node:

 npm init --yes 

Teraz zainstaluj te pakiety.

 npm install apollo-server graphql mongoose jsonwebtokens dotenv 

Na koniec utwórz plik server.js w katalogu głównym i skonfiguruj serwer za pomocą tego kodu:

 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);
  });

Serwer GraphQL jest skonfigurowany z parametrami typeDefs i resolwerami, określającymi schemat i operacje, które może obsłużyć API. Opcja kontekstu konfiguruje obiekt req w kontekście każdego mechanizmu rozpoznawania nazw, co umożliwi serwerowi dostęp do szczegółów specyficznych dla żądania, takich jak wartości nagłówka.

Utwórz bazę danych MongoDB

Aby nawiązać połączenie z bazą danych, najpierw utwórz bazę danych MongoDB lub skonfiguruj klaster w MongoDB Atlas. Następnie skopiuj podany ciąg URI połączenia z bazą danych, utwórz plik .env i wprowadź parametry połączenia w następujący sposób:

 MONGO_URI="<mongo_connection_uri>"

Zdefiniuj model danych

Zdefiniuj model danych za pomocą Mongoose. Utwórz nowy plik models/user.js i dołącz następujący kod:

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

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

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

Zdefiniuj schemat GraphQL

W interfejsie API GraphQL schemat definiuje strukturę danych, które można odpytywać, a także przedstawia dostępne operacje (zapytania i mutacje), które można wykonać w celu interakcji z danymi za pośrednictwem interfejsu API.

Aby zdefiniować schemat, utwórz nowy folder w katalogu głównym projektu i nadaj mu nazwę graphql. Wewnątrz tego folderu dodaj dwa pliki: typeDefs.js i resolrs.js.

W pliku typeDefs.js umieść następujący kod:

 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;

Utwórz programy tłumaczące dla API GraphQL

Funkcje przeliczające określają sposób pobierania danych w odpowiedzi na zapytania klientów i mutacje, a także inne pola zdefiniowane w schemacie. Kiedy klient wysyła zapytanie lub mutację, serwer GraphQL uruchamia odpowiednie mechanizmy rozpoznawania nazw w celu przetworzenia i zwrócenia wymaganych danych z różnych źródeł, takich jak bazy danych lub interfejsy API.

Aby zaimplementować uwierzytelnianie i autoryzację przy użyciu tokenów sieciowych JSON (JWT), zdefiniuj mechanizmy rozpoznawania nazw dla mutacji rejestru i logowania. Będą one obsługiwać procesy rejestracji i uwierzytelniania użytkowników. Następnie utwórz narzędzie do rozwiązywania zapytań pobierania danych, które będzie dostępne tylko dla uwierzytelnionych i autoryzowanych użytkowników.

Najpierw jednak zdefiniuj funkcje służące do generowania i weryfikowania tokenów JWT. Zacznij od dodania następujących importów w pliku resolrs.js.

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

Pamiętaj, aby dodać tajny klucz, którego będziesz używać do podpisywania tokenów internetowych JSON do pliku .env.

 SECRET_KEY = '<my_Secret_Key>'; 

Aby wygenerować token uwierzytelniający, należy uwzględnić poniższą funkcję, która określa także unikalne atrybuty tokena JWT, np. czas ważności. Dodatkowo możesz uwzględnić inne atrybuty, takie jak wydawane na czas, w zależności od konkretnych wymagań aplikacji.

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

  return token;
}

Teraz zaimplementuj logikę weryfikacji tokenu, aby sprawdzić tokeny JWT zawarte w kolejnych żądaniach 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');
  }
}

Ta funkcja pobierze token jako dane wejściowe, zweryfikuje jego ważność przy użyciu określonego tajnego klucza i zwróci zdekodowany token, jeśli jest prawidłowy, w przeciwnym razie zgłasza błąd wskazujący nieprawidłowy token.

Zdefiniuj programy do rozpoznawania API

Aby zdefiniować mechanizmy rozpoznawania nazw dla API GraphQL, musisz określić konkretne operacje, którymi będzie zarządzał, w tym przypadku operacje rejestracji i logowania użytkownika. Najpierw utwórz obiekt funkcji rozpoznawania nazw, który będzie przechowywał funkcje mechanizmu rozpoznawania nazw, a następnie zdefiniuj następujące operacje mutacji:

 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');
      }
    }
  },

Mutacja rejestru obsługuje proces rejestracji poprzez dodanie nowych danych użytkownika do bazy danych. Podczas gdy mutacja logowania zarządza logowaniem użytkowników — po pomyślnym uwierzytelnieniu wygeneruje token JWT, a także zwróci komunikat o powodzeniu w odpowiedzi.

Teraz dołącz moduł rozwiązywania zapytań do pobierania danych użytkownika. Aby mieć pewność, że to zapytanie będzie dostępne tylko dla uwierzytelnionych i autoryzowanych użytkowników, uwzględnij logikę autoryzacji, aby ograniczyć dostęp tylko do użytkowników z rolą administratora.

Zasadniczo zapytanie najpierw sprawdzi ważność tokena, a następnie rolę użytkownika. Jeśli sprawdzenie autoryzacji zakończy się pomyślnie, zapytanie mechanizmu rozpoznawania nazw rozpocznie pobieranie i zwracanie danych użytkowników z bazy danych.

   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');
      }
    },
  },
};

Na koniec uruchom serwer deweloperski:

 node server.js 

Wspaniały! Teraz śmiało przetestuj funkcjonalność interfejsu API za pomocą piaskownicy API serwera Apollo w swojej przeglądarce. Na przykład możesz użyć mutacji rejestru, aby dodać nowe dane użytkownika do bazy danych, a następnie mutacji logowania w celu uwierzytelnienia użytkownika.

Na koniec dodaj token JWT do sekcji nagłówka autoryzacji i kontynuuj wysyłanie zapytań do bazy danych o dane użytkownika.

Zabezpieczanie API GraphQL

Uwierzytelnianie i autoryzacja to kluczowe elementy zabezpieczające interfejsy API GraphQL. Niemniej jednak należy mieć świadomość, że same one mogą nie wystarczyć do zapewnienia kompleksowego bezpieczeństwa. Powinieneś wdrożyć dodatkowe środki bezpieczeństwa, takie jak sprawdzanie poprawności danych wejściowych i szyfrowanie wrażliwych danych.

Przyjmując kompleksowe podejście do bezpieczeństwa, możesz zabezpieczyć swoje interfejsy API przed różnymi potencjalnymi atakami.