Cách bảo mật GraphQL API: Triển khai xác thực người dùng trong Express.js bằng JWT

JWT cung cấp một giải pháp đơn giản để xử lý lỗi xác thực phức tạp. Dưới đây là hướng dẫn cách bảo mật GraphQL API.

GraphQL API

GraphQL là một giải pháp thay thế phổ biến cho kiến trúc RESTful API truyền thống, cung cấp truy vấn dữ liệu linh hoạt và hiệu quả cho API. Với sự phổ biến ngày càng tăng, ưu tiên bảo mật cho GraphQL API ngày càng quan trọng để bảo vệ ứng dụng khỏi truy cập bất hợp pháp và vi phạm dữ liệu tiềm ẩn.

Một trong số phương pháp bảo mật GraphQL API hiệu quả là triển khai JSON Web Tokens (JWT). JWT cung cấp phương thức an toàn và hiệu quả cho việc cấp quyền truy cập tới các tài nguyên được bảo vệ và thực hiện các hành động được ủy quyền, đảm bảo giao tiếp an toàn giữa client và API.

Xác thực và ủy quyền trong GraphQL API

Khác REST API, GraphQL API thường có một endpoint duy nhất, cho phép client tự động yêu cầu lượng dữ liệu khác nhau trong truy vấn của họ. Dù tính linh hoạt là điểm mạnh của nó, nó cũng tăng rủi ro bị tấn công bảo mật như các lỗ hổng kiểm soát quyền truy cập.

Lỗ hổng kiểm soát quyền truy cập

Để giảm thiểu rủi ro này, điều quan trọng bạn cần làm là triển khai quá trình xác thực và phân quyền mạnh mẽ, bao gồm cấp quyền phù hợp. Bằng cách làm việc này, bạn đảm bảo chỉ người dùng có thẩm quyền mới xem được tài nguyên được bảo vệ, từ đó, giảm rủi ro bị tẩy xóa hay thất thoát dữ liệu.

Thiết lập server Express.js Apollo

Apollo Server là một triển khai server GraphQL được dùng rộng rãi cho GraphQL API. Bạn có thể dùng nó để dễ dàng xây dựng các schema GraphQL, xác định resolver và quản lý các nguồn dữ liệu khác cho API của bạn.

Để thiết lập một Express.js Apollo, tạo và mở thư mục dự án:

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

Tiếp theo, chạy lệnh này để khởi tạo một dự án Node.js mới bằng npm, trình quản lý gói Node:

npm init --yes

Giờ, cài đặt những package này:

npm install apollo-server graphql mongoose jsonwebtokens dotenv

Cuối cùng, tạo một file server.js ở thư mục gốc, và thiết lập server của bạn bằng code:

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

Server GraphQL được thiết lập với các tham số typeDefsresolvers, xác định schema và các hoạt động mà API có thể xử lý. Tùy chọn context cấu hình đối tượng req theo ngữ cảnh của từng resolver, mà cho phép server đó truy cập các chi tiết truy vấn cụ thể như các giá trị header.

Tạo database MongoDB

Để thiết lập kết nối database, đầu tiên tạo database MongoDB hoặc thiết lập một nó trên MongoDB Atlas. Sau đó, sao chép kết nối chuỗi URL kết nối database được cung cấp, tạo một file .env và nhập chỗi kết nối như sau:

MONGO_URI="<mongo_connection_uri>"

Xác định mẫu dữ liệu

Xác định một mẫu dữ liệu bằng Mongoose. Tạo một file models/user.js mới và bao gồm code sau:

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

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

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

Xác định schema GraphQL

Trong GraphQL API, schema xác định cấu trúc dữ liệu có thể được truy vấn, cũng như phác thảo các hoạt động sẵn có (truy vấn và đột biến) mà bạn có thể thực hiện tương tác với dữ liệu qua API.

Để xác định schema, tạo một thư mục mới trong danh mục gốc của dự án và đặt tên nó là graphql. Bên trong thư mục này, thêm hai file: typeDefs.js resolvers.js.

Trong file typeDefs.js, bao gồm code sau:

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;

Tạo resolvers cho GraphQL API

Các hàm resolver xác định cách truy xuất dữ liệu để phản hồi các truy vấn và đột biến của client, cũng như các trường khác được xác định trong schema. Khi client gửi một truy vấn hoặc đột biến, máy chủ GraphQL sẽ kích hoạt các trình phân giải tương ứng để xử lý và trả về dữ liệu cần thiết từ nhiều nguồn khác nhau, chẳng hạn như cơ sở dữ liệu hoặc API.

Để tiến hành xác thực và ủy quyền bằng JSON Web Tokens (JWT), xác định các resolver cho những đột biến đăng ký và đăng nhập. Chúng sẽ xử lý các quá trình đăng ký và xác thực cho người dùng. Sau đó, tạo một resolver truy vấn tìm nạp dữ liệu mà chỉ những người dùng được xác thực và ủy quyền mới có thể truy cập được.

Thế nhưng, trước tiên, xác định các hàm để tạo và xác thực JWT. Trong file resolvers.js, bắt đầu bằng cách thêm các import sau.

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

Đảm bảo thêm key bí mật bạn sẽ dùng để đăng ký token web JSON vào file .env.

SECRET_KEY = '<my_Secret_Key>';

Để tạo một token xác thực, bao gồm hàm sau, hàm này cũng chỉ định các thuộc tính duy nhất cho mã thông báo JWT, chẳng hạn như thời gian hết hạn. Ngoài ra, bạn có thể kết hợp các thuộc tính khác như được phát hành vào thời điểm dựa trên yêu cầu ứng dụng cụ thể của bạn.

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

  return token;
}

Giờ, triển khai logic xác thực token để xác minh các token được bao gồm trong truy vấn HTTP tiếp theo.

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

Hàm này sẽ lấy một token làm đầu vào, xác minh tính hợp lệ của nó bằng key bí mật được chỉ định và trả về token đã giải mã nếu nó hợp lệ. Nếu không, báo lỗi cho biết token không hợp lệ.

Xác định resolver API

Để xác minh resolver cho GraphQL API, bạn cần vạch ra các họa động cụ thể mà nó sẽ quản lý. Ở đây là đăng ký người dùng và hoạt động đăng nhập. Đầu tiên, tạo một đối tượng resolvers chứa các hàm resolver, sau đó xác định những hoạt động đột biến sau:

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

Mutation register xử lý quá trình đăng ký bằng cách thêm dữ liệu người dùng mới vào database. Trong khi mutation login quản lý đăng nhập người dùng - về mặt xác thực thành công, nó sẽ tạo một token JWT, cũng như trả về một thông báo thành công trong phản hồi.

Giờ, bao gồm resolver truy vấn để truy xuất dữ liệu người dùng. Để đảm bảo truy vấn này sẽ chỉ có thể truy cập tới người dùng đã xác thực và có thẩm quyền, bao gồm logic ủy quyền để hạn chế truy cập chỉ tới những người dùng có quyền Admin.

Về cơ bản, truy vấn trước tiên sẽ kiểm tra tính hợp lệ của token, sau đó tới vai trò của người dùng. Nếu kiểm tra quyền hạn thành công, truy vấn resolver sẽ tiếp tục tìm nạp và trả về dữ liệu người dùng từ 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');
      }
    },
  },
};

Cuối cùng, khởi động server lập trình:

node server.js

Giờ tới giai đoạn kiểm tra chức năng của API bằng sandbox Apollo Server API trong trình duyệt của bạn. Ví dụ, bạn có thể dùng mutation register để thêm dữ liệu người dùng mới trong database, rồi mutation login để xác thực người dùng.

Login

Cuối cùng thêm token JWT vào phần header ủy quyền và tiếp tục truy vấn database cho dữ liệu người dùng.

Truy vấn dữ liệu người dùng

Thế là xong! Hi vọng bài viết hữu ích với các bạn.

Thứ Tư, 25/10/2023 08:12
51 👨 213
0 Bình luận
Sắp xếp theo