Contents

Jak zabezpieczyć interfejsy API GraphQL: Wdrażanie uwierzytelniania użytkowników w Express.js przy użyciu 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 jego rosnącą popularnością, coraz ważniejsze staje się priorytetowe traktowanie bezpieczeństwa interfejsów API GraphQL w celu ochrony aplikacji przed nieautoryzowanym dostępem i potencjalnymi naruszeniami danych.

Jedna z realnych strategii ochrony interfejsów API GraphQL obejmuje wykorzystanie tokenów sieciowych JSON (JWT), które oferują skuteczny sposób przyznawania dostępu do ograniczonych zasobów i wykonywania uprzywilejowanych operacji przy jednoczesnym zachowaniu bezpiecznej komunikacji między klientami a interfejsami API.

Uwierzytelnianie i autoryzacja w interfejsach API GraphQL

W przeciwieństwie do interfejsów programowania aplikacji (API) RESTful, implementacje interfejsów API GraphQL często zawierają pojedynczy punkt końcowy, który umożliwia klientom żądanie różnych ilości i typów informacji za pośrednictwem zapytań. Ta zdolność adaptacji stanowi zarówno zaletę, jak i potencjalne źródło zagrożeń bezpieczeństwa, szczególnie w odniesieniu do zagrożonych mechanizmów kontroli dostępu.

/pl/images/altumcode-dc6pb2jdaqs-unsplash-1.jpg

Aby skutecznie zarządzać wspomnianym wyżej zagrożeniem, kluczowe jest ustanowienie kompleksowych protokołów uwierzytelniania i autoryzacji, które obejmują skrupulatne określanie uprawnień dostępu. Dzięki takim środkom zapewniamy, że tylko upoważnione osoby mają dostęp do wrażliwych zasobów, minimalizując w ten sposób prawdopodobieństwo wystąpienia luk w zabezpieczeniach i incydentów wycieku danych.

[wstaw tutaj link do repozytorium GitHub].

Konfiguracja serwera Express.js Apollo Server

Apollo Server to szeroko stosowana implementacja serwera GraphQL dla interfejsów API GraphQL. Można go używać do łatwego tworzenia schematów GraphQL, definiowania resolverów i zarządzania różnymi źródłami danych dla interfejsów API.

Utworzenie serwera Express.js Apollo wymaga utworzenia i otwarcia katalogu projektu.

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

Aby zainicjować nowy projekt Node.js przy użyciu Menedżera pakietów Node (npm), wykonaj następujące polecenie:

 npm init --yes 

Teraz zainstaluj następujące pakiety.

 npm install apollo-server graphql mongoose jsonwebtokens dotenv 

Rzeczywiście, konieczne jest wygenerowanie pliku server.js w głównym folderze projektu i skonfigurowanie serwera za pomocą dostarczonego skryptu w następujący sposób:

 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 został skonfigurowany przy użyciu kombinacji typeDefs i resolverów , które definiują schemat i operacje, które API jest w stanie obsłużyć.Dodatkowo, użycie opcji context pozwala na konfigurację obiektu req w celu uwzględnienia określonego kontekstu odpowiedniego dla każdego indywidualnego resolvera. Umożliwia to serwerowi dostęp do informacji specyficznych dla żądania, w tym wartości nagłówka, w celu ułatwienia bardziej wydajnego wyszukiwania danych.

Utwórz bazę danych MongoDB

albo utwórz nową bazę danych w lokalnej instalacji MongoDB, albo skorzystaj z rozwiązania opartego na chmurze oferowanego przez MongoDB Atlas, konfigurując klaster i uzyskując niezbędne informacje o połączeniu. Po uzyskaniu odpowiedniego ciągu URI połączenia można przystąpić do utworzenia nowego pliku .env i wprowadzenia szczegółów połączenia w określonym formacie.

 MONGO_URI="<mongo_connection_uri>"

Definiowanie modelu danych

Jasne, oto przykład definiowania modelu danych dla użytkowników w pliku schematu Mongoose o nazwie models/user.js :javascriptconst mongoose = require(‘mongoose’);// Definiowanie schematu użytkownikaaconst userSchema = new mongoose.Schema({name: { type: String, required: true },email: { type: String, unique: true, required: true },password: { type: String, required: true },});// Kompilujemy schemat do modeluconst User = mongoose.model(‘User’, userSchema);module.exports = User;W tym przykładzie najpierw importujemy bibliotekę Mongoose na górze pliku. Następnie tworzymy nowy obiekt Schema

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

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

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

Definiowanie schematu GraphQL

Schemat GraphQL API określa układ informacji, które można pobrać, a także określa możliwe operacje (zapytania i zmiany), które można wykonać w celu komunikacji z danymi za pośrednictwem wspomnianego interfejsu.

Aby ustanowić schemat dla swojego projektu, należy najpierw utworzyć nowy folder w katalogu głównym swojego przedsięwzięcia, który będzie określany jako “graphql”. Ten konkretny folder będzie służył jako enklawa, w której zorganizowane są wszystkie komponenty związane z operacjami GraphQL. W ramach folderu “graphql” muszą istnieć dwa odrębne dokumenty - jeden o nazwie “typeDefs.js”, a drugi zatytułowany “resolvers.js”.

W pliku typeDefs.js należy umieścić 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; 

Create Resolvers for the GraphQL API

Funkcje resolver odgrywają istotną rolę w określaniu sposobu uzyskiwania danych w odpowiedzi na żądania klienta dotyczące informacji i modyfikacji, w tym wszelkich dodatkowych szczegółów określonych w schemacie.Po otrzymaniu żądania od klienta serwer GraphQL aktywuje odpowiednie resolvery w celu uzyskania danych z wielu źródeł, w tym baz danych i usług zewnętrznych, i odpowiednio je zwraca.

Aby włączyć uwierzytelnianie i kontrolę dostępu przy użyciu JSON Web Token (JWT) w naszym serwerze GraphQL, musimy najpierw ustanowić resolvery zarówno dla mutacji “register”, jak i “login”. Te resolvery są odpowiedzialne za zarządzanie procedurami tworzenia i weryfikacji kont. Następnie powinniśmy opracować resolver zapytań o pobieranie danych, do którego dostęp mogą uzyskać tylko zarejestrowani i zweryfikowani użytkownicy, którym przyznano do tego uprawnienia.

Koder i dekoder RSA JWE z pakietu “node-jose”, a także moduł “crypto” dostarczany natywnie przez Node.js. Umożliwi nam to płynne wykonywanie operacji kryptograficznych związanych z generowaniem i walidacją JWT. W ten sposób możemy zapewnić bezpieczną komunikację między naszą aplikacją a jej użytkownikami, zachowując jednocześnie prostotę i łatwość użytkowania zarówno dla programistów, jak i użytkowników końcowych.

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

Upewnij się, że dołączasz tajny klucz wymagany do podpisywania JSON Web Tokens w pliku .env, aby prawidłowo go wykorzystać.

 SECRET_KEY = '<my_Secret_Key>'; 

Aby utworzyć prawidłowy token uwierzytelniający, konieczne jest wykorzystanie dostarczonej funkcji i określenie niestandardowych cech, w tym czasu wygaśnięcia, w tokenie JWT. Co więcej, dodatkowe atrybuty, takie jak czas wydania, mogą być uwzględnione w oparciu o szczególne potrzeby aplikacji.

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

  return token;
} 

Aby zapewnić autentyczność tokenów JSON Web Token (JWT) wykorzystywanych w przyszłych wymianach HTTP, konieczne jest włączenie procesu walidacji tych tokenów. Zostanie to osiągnięte poprzez wdrożenie mechanizmu uwierzytelniania, który sprawdza i potwierdza legalność każdego JWT przedstawionego podczas takich interakcji.

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

Podana funkcja akceptuje token jako dane wejściowe, wykorzystując wyznaczony tajny klucz do sprawdzenia jego autentyczności. Jeśli token okaże się prawidłowy, funkcja zwraca odszyfrowany token; jednak w przypadku jakichkolwiek rozbieżności zgłasza wyjątek oznaczający obecność nieprawidłowego tokena.

Definiowanie resolwerów API

Aby określić proces rozwiązywania dla naszego interfejsu API GraphQL, musimy określić konkretne zadania lub działania, które będzie on podejmował, takie jak zarządzanie procesami rejestracji i logowania użytkowników. Na początku utwórz obiekt zawierający funkcje resolvera, które służą do wykonywania tych wyznaczonych zadań.Następnie zaimplementuj następujące mutacje, aby ułatwić te operacje:

 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 register jest odpowiedzialna za obsługę procesu rejestracji poprzez dodanie nowych danych użytkownika do bazy danych. Z drugiej strony, mutacja login zajmuje się logowaniem użytkowników i po pomyślnym uwierzytelnieniu generuje JSON Web Token (JWT), jednocześnie zwracając komunikat o powodzeniu w odpowiedzi.

Aby ograniczyć dostęp do zapytania pobierającego dane użytkownika wyłącznie do uwierzytelnionych i uprzywilejowanych użytkowników posiadających rolę “Admin”, konieczne jest włączenie logiki autoryzacji do systemu. Gwarantuje to, że nieupoważnione osoby nie będą w stanie uzyskać dostępu do poufnych informacji.

Zasadniczo zapytanie początkowo zweryfikuje legalność tokena, a następnie oceni rolę użytkownika. W przypadku, gdy badanie autoryzacji okaże się korzystne, żądanie rozwiązania pobierze i przekaże informacje o użytkownikach końcowych z repozytorium.

   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 należy uruchomić serwer deweloperski:

 node server.js 

Rzeczywiście, zdecydowanie zaleca się sprawdzenie działania naszego interfejsu API GraphQL poprzez zastosowanie piaskownicy API Apollo Server w przeglądarce internetowej. Aby zilustrować ten proces, rozważ wykorzystanie mutacji “register” do wprowadzenia nowych informacji o użytkowniku do bazy danych, a następnie mutacji “login” do celów uwierzytelniania.

/pl/images/login-mutation.jpg

Na koniec należy uwzględnić token JWT w parametrze nagłówka autoryzacji, a następnie pobrać informacje o użytkowniku z bazy danych, wykonując zapytanie.

/pl/images/graphql-api-query.jpg

Zabezpieczanie interfejsów API GraphQL

Uwierzytelnianie i autoryzacja odgrywają kluczową rolę w zapewnianiu bezpieczeństwa interfejsów API GraphQL; jednak samo ich wdrożenie nie gwarantuje pełnej ochrony. Niezbędne jest włączenie dalszych środków bezpieczeństwa, takich jak solidna walidacja danych wejściowych i szyfrowanie poufnych informacji w celu osiągnięcia pełnego bezpieczeństwa.

Wdrożenie kompleksowej strategii bezpieczeństwa jest niezbędne do ochrony interfejsów programowania aplikacji (API) przed różnymi rodzajami zagrożeń, które mogą się pojawić.