Реалізація автентифікації користувача в Express.js за допомогою JWT

GraphQL стає все більш популярною альтернативою традиційним RESTful API, пропонуючи більш гнучкий та ефективний спосіб запиту та обробки даних. У зв’язку з його поширенням, забезпечення безпеки GraphQL API стає критично важливим для захисту додатків від несанкціонованого доступу та потенційних витоків даних.

Одним з дієвих методів захисту GraphQL API є використання JSON Web Tokens (JWT). JWT є безпечним та ефективним способом надання доступу до захищених ресурсів, а також для виконання авторизованих дій, гарантуючи безпечну взаємодію між клієнтами та API.

Автентифікація та авторизація в GraphQL API

На відміну від REST API, GraphQL API зазвичай мають лише одну кінцеву точку, що дозволяє клієнтам гнучко запитувати різні обсяги даних в одному запиті. Ця гнучкість є перевагою, але вона також підвищує ризик атак, особливо пов’язаних з недостатнім контролем доступу.

Щоб мінімізувати ці ризики, необхідно запровадити надійні механізми автентифікації та авторизації, зокрема, ретельно визначити права доступу. Це гарантує, що лише авторизовані користувачі матимуть доступ до захищених ресурсів, і таким чином зменшується ймовірність порушення безпеки та втрати даних.

Код цього проекту можна знайти у його GitHub репозиторії.

Налаштування сервера Express.js Apollo

Apollo Server є популярною реалізацією GraphQL сервера для GraphQL API. Він спрощує створення GraphQL схем, визначення резолверів та управління різними джерелами даних для ваших API.

Для налаштування Express.js Apollo Server, спочатку створіть та відкрийте папку проекту:

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

Далі, скористайтеся командою, щоб ініціалізувати новий проект Node.js за допомогою npm, менеджера пакетів Node:

 npm init --yes 

Тепер встановіть необхідні пакети:

 npm install apollo-server graphql mongoose jsonwebtokens dotenv 

Нарешті, створіть файл `server.js` у кореневому каталозі та налаштуйте сервер, додавши наступний код:

 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("Підключено до бази даних");
    return server.listen({ port: 5000 });
  })
  .then((res) => {
    console.log(`Сервер працює за адресою ${res.url}`);
  })
  .catch(err => {
    console.log(err.message);
  });

Сервер GraphQL налаштований з використанням параметрів `typeDefs` та `resolvers`, які визначають схему та операції, які API може обробляти. Параметр `context` налаштовує об’єкт `req` відповідно до контексту кожного резолвера, дозволяючи серверу отримувати доступ до деталей запиту, таких як значення заголовків.

Створення бази даних MongoDB

Для підключення до бази даних, спочатку створіть базу даних MongoDB або налаштуйте кластер на MongoDB Atlas. Скопіюйте наданий рядок URI підключення до бази даних, створіть файл `.env` та введіть рядок підключення наступним чином:

 MONGO_URI="<mongo_connection_uri>"

Визначення моделі даних

Визначте модель даних, використовуючи Mongoose. Створіть новий файл `models/user.js` і додайте наступний код:

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

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

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

Визначення схеми GraphQL

У GraphQL API схема визначає структуру даних, які можуть бути запитувані, та описує доступні операції (запити та мутації), що можуть бути використані для взаємодії з даними через API.

Для визначення схеми, створіть нову папку в кореневому каталозі вашого проекту та назвіть її `graphql`. У цій папці додайте два файли: `typeDefs.js` та `resolvers.js`.

У файл `typeDefs.js` додайте наступний код:

 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;

Створення резолверів для GraphQL API

Функції розв’язувача визначають, як дані отримуються у відповідь на запити клієнта та мутації, а також для інших полів, визначених у схемі. Коли клієнт надсилає запит або мутацію, сервер GraphQL викликає відповідні резолвери для обробки та повернення необхідних даних з різних джерел, наприклад, баз даних чи API.

Для реалізації автентифікації та авторизації з використанням JSON Web Tokens (JWT), визначте резолвери для мутацій реєстрації та входу. Вони будуть обробляти процеси реєстрації та автентифікації користувачів. Потім створіть резолвер для запитів отримання даних, який буде доступним лише для авторизованих користувачів.

Але спочатку визначимо функції для створення та перевірки JWT. У файл `resolvers.js` почніть з додавання наступних імпортів:

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

Переконайтеся, що ви додали секретний ключ, який використовуватиметься для підпису JSON Web Tokens, у файл `.env`:

 SECRET_KEY = '<my_Secret_Key>'; 

Щоб згенерувати токен автентифікації, додайте наступну функцію, яка визначає унікальні атрибути для JWT, наприклад, час закінчення терміну дії. Ви також можете включити інші атрибути, на основі вимог вашого додатку.

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

  return token;
}

Тепер реалізуйте логіку перевірки токенів для валідації JWT, які включені у HTTP запитах.

 function verifyToken(token) {
  if (!token) {
    throw new Error('Токен не надано');
  }

  try {
    const decoded = jwt.verify(token, secretKey, { algorithms: ['HS256'] });
    return decoded;
  } catch (err) {
    throw new Error('Недійсний токен');
  }
}

Ця функція приймає токен як вхідні дані, перевіряє його дійсність за допомогою вказаного секретного ключа і повертає розкодований токен, якщо він дійсний. В іншому випадку, повертає помилку, яка вказує на недійсний токен.

Визначення API Resolvers

Для визначення резолверів для GraphQL API необхідно окреслити конкретні операції, якими він керуватиме. У цьому випадку це операції реєстрації та входу користувача. Спочатку створимо об’єкт резолверів, який міститиме функції резолвера, а потім визначимо наступні операції мутації:

 const resolvers = {
  Mutation: {
    register: async (_, { userInput: { name, password, role } }) => {
      if (!name || !password || !role) {
        throw new Error('Ім’я, пароль та роль обов’язкові');
     }

      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('Не вдалося створити користувача');
      }
    },
    login: async (_, { name, password }) => {
      try {
        const user = await User.findOne({ name: name });

        if (!user) {
          throw new Error('Користувача не знайдено');
       }

        if (password !== user.password) {
          throw new Error('Неправильний пароль');
        }

        const token = generateToken(user);

        if (!token) {
          throw new Error('Не вдалося створити токен');
        }

        return {
          message: 'Вхід успішний',
          token: token,
        };
      } catch (error) {
        console.error(error);
        throw new Error('Помилка входу');
      }
    }
  },

Мутація `register` керує процесом реєстрації шляхом додавання нових даних користувача до бази даних. Мутація `login` обробляє вхід користувача. У разі успішної автентифікації, вона створює токен JWT та повертає повідомлення про успішний вхід у відповідь.

Тепер додайте резолвер запитів для отримання даних користувача. Щоб переконатися, що запит доступний лише для автентифікованих та авторизованих користувачів, додайте логіку авторизації для обмеження доступу лише для користувачів з роллю адміністратора.

По суті, запит спочатку перевіряє дійсність токена, а потім роль користувача. Якщо перевірка авторизації пройшла успішно, запит резолвера продовжує отримання та повернення даних користувачів з бази даних.

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

        if (decodedToken.role !== 'Admin') {
          throw new Error('Не авторизовано. Тільки адміністратори можуть отримати доступ до цих даних.');
        }

        const users = await User.find({}, { name: 1, _id: 1, role:1 });
        return users;
      } catch (error) {
        console.error(error);
        throw new Error('Не вдалося отримати користувачів');
      }
    },
  },
};

Нарешті, запустіть сервер розробки:

 node server.js 

Чудово! Тепер перевірте функціональність API за допомогою пісочниці API Apollo Server у браузері. Наприклад, ви можете скористатися мутацією `register`, щоб додати нові дані користувача в базу даних, а потім мутацією `login` для автентифікації користувача.

Нарешті, додайте токен JWT до розділу заголовка авторизації та перейдіть до запиту даних користувача з бази даних.

Забезпечення безпеки GraphQL API

Автентифікація та авторизація є ключовими компонентами для захисту GraphQL API. Однак, важливо пам’ятати, що їх самих по собі може бути недостатньо для забезпечення повної безпеки. Вам необхідно застосовувати додаткові заходи безпеки, такі як валідація введених даних та шифрування конфіденційних даних.

Застосовуючи комплексний підхід до безпеки, ви зможете захистити свої API від різноманітних потенційних атак.