+18

[Authentication] Xác thực với JWT cùng mã hóa Đối xứng và Bất đối xứng (Phần 2)

Trong bài trước, chúng ta đã cùng tìm hiểu về các kiến thức cơ bản và cơ chế hoạt động của JWT. Trong bài này, anh em mình sẽ tiếp tục cùng nhau tìm hiểu về một phần mà mình nghĩ là hay nhất khi đề cập đến JWT, đó là áp dụng Mã hóa đối xứng và Mã hóa bất đối xứng trong xác thực JWT.

Mình sẽ sử dụng NodeJSMongoDB để triển khai việc mã hóa cho JWT. Còn về ý tưởng, cách thức áp dụng thì anh em dùng ngôn ngữ lập trình nào cũng chơi được hết nhé.

Xác thực JWT cùng Mã hóa đối xứng và Mã hóa bất đối xứng

1. Xác thực JWT cùng Mã hóa đối xứng

Mã hóa đối xứng là phương thức mã hóa dữ liệu sử dụng cùng một key vừa để mã hóa và vừa để giải mã thông tin.
Cách làm này chắc không còn xa lạ gì với anh em khi ban đầu tiếp xúc với JWT.

Cách triển khai cơ bản như sau:

z4224827978942_f37dc2546f9e320d7d8b34ba37bbef00.jpg

Đối với cách triển khai này, chúng ta tạo một JWT với một payload chứa thông tin về userid và email.

  1. Chúng ta sử dụng phương thức jwt.sign() để mã hóa thông tin này với key secretKey và thời gian sống của token là 1 giờ.
  2. Sau đó, chúng ta sử dụng phương thức jwt.verify() để xác thực JWT với key secretKey và lấy thông tin.

Anh em tham khảo source code GIT ở đây nhé.

B1: Import thư viện và khởi tạo secretKey

// jwt.aes.js
// Import thư viện và khởi tạo secretKey
const jwt = require('jsonwebtoken');
const secretKey = 'mysupersecretkey';

B2: Tạo hàm tạo JWT

// Hàm tạo JWT
const genToken = (userInfo) => {
    const accessToken = jwt.sign({ userid: userInfo._id, email: userInfo.email }, secretKey, { expiresIn: '1h' });
    return accessToken;
}

B3: Tạo hàm xác thực JWT

// Hàm xác thực JWT
const validateToken = async(accessToken) => {
    jwt.verify(accessToken, secretKey, (err, decode) => {
        if(err) {
            console.error('error verify token');
        } else{
            console.log('decode jwt::', decode);
        }
    });
}

B4: Kiểm tra kết quả

// Thực thi
async function runScript() {
    const userId = 1;
    const accessToken = genToken({_id: userId, email: 'pdthien@gmail.com'});
    await validateToken(accessToken);
}
runScript();
  • Ưu điểm:
    • Dễ thực hiện vì sử dụng 1 key duy nhất để vừa mã hóa và giải mã thông tin.
    • Giảm áp lực cho phía server vì không phải lưu thêm dữ liệu gì cho việc xác thực.
  • Nhược điểm: Không an toàn vì khi hacker ăn cắp được key đó, sẽ dễ dàng tạo ra các fake JWT, có thể truy cập tấn công ứng dụng.

2. Xác thực JWT cùng Mã hóa bất đối xứng

Mã hóa bất đối xứng sử dụng 2 key khác nhau (public keyprivate key) để mã hóa và giải mã thông tin. Trong đó, public key được chia sẻ với mọi người và private key được giữ bí mật.

  • Private key được sử dụng để mã hóa (sign) thông tin tạo ra JWT. Sau khi tạo token xong, Private key hoàn toàn biến mất khỏi hệ thống.
  • Public key được sử dụng để giải mã (verify) JWT đó, không có chiều ngược lại. Nên dù hacker có lấy được Public key cũng không thể tạo được fake JWT truy cập được ứng dụng.

Cách thức triển khai như sau:

feef4e0f664dba13e35c.jpg

Đối với cách triển khai này, chúng ta tạo một JWT với một payload chứa thông tin về userid và email

  1. Chúng ta sử dụng MongoDB để tạo bảng KeyToken lưu userId và publicKey.
  2. Tạo 2 key privateKeypublicKey sử dụng thuật toán rsa thông qua thư viện crypto.
  3. Chúng ta sử dụng phương thức jwt.sign() để mã hóa thông tin này với key privateKey và thời gian sống của token là 1 giờ (privateKey sau đó biến mất hoàn toàn khỏi hệ thống).
  4. Chúng ta sử dụng phương thức jwt.verify() để xác thực JWT với key publicKey và lấy thông tin.

Anh em tham khảo source code GIT ở đây nhé.

B1: Khởi tạo kết nối MongoDB

// init.mongodb.js
const mongoose = require("mongoose");
const connnectString = 'mongodb://127.0.0.1:27017/jwt-auth';

mongoose.connect(connnectString)
.then(_ => console.log('mongoDB connected'))
.catch(err => console.log('mongDB error ' + err));

module.exports = mongoose;

B2: Tạo bảng (collection) Key lưu trữ Public key

// keytoken.model.js
const { Schema, model } = require('mongoose');

const DOCUMENT_NAME = 'Key';
const COLLECTION_NAME = 'Keys';

var keyTokenSchema = new Schema({
    userid: {
        type: Number,
        required: true
    },
    publicKey: {
        type: String,
        require: true
    }
}, {
    timestamps: true,
    collection: COLLECTION_NAME
});

module.exports = model(DOCUMENT_NAME, keyTokenSchema);

B3: Tạo hàm genToken. Trong hàm xử lý 2 việc: Lưu public key vào DB + Dùng privateKey để mã hóa tạo JWT

// jwt.rsa.js
// Import thư viện và các biến
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
require('./init.mongodb');
const keytokenModel = require("./keytoken.model.js");
// Hàm xử lý lưu public key vào DB
const createKeyToken = async({userid, publicKey}) => {
    const publicKeyString = publicKey.toString();
    const filter = {userid: userid};
    const update = {userid: userid, publicKey: publicKeyString};
    const options = {upsert: true, new: true};
    await keytokenModel.findOneAndUpdate(filter, update, options);
}
// Hàm tạo JWT từ private key
const createAccessToken = async (payload, privateKey) => {
    const accessToken = await jwt.sign(payload, privateKey, {
        algorithm: 'RS256',
        expiresIn: '1h'
    });
    return accessToken;
}
// Hàm genToken. Xử lý 2 việc: Lưu public key vào DB + Dùng private key để mã hóa tạo JWT.
const genToken = async(userInfo) => {
    // Dùng rsa để tạo privateKey và publicKey
    const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
        modulusLength: 4096,
        publicKeyEncoding:  { type: 'pkcs1', format: 'pem' },
        privateKeyEncoding: { type: 'pkcs1', format: 'pem' }
    })
    // Lưu userid và publicKey vào bảng KeyToken
    await createKeyToken({ userid: userInfo._id, publicKey })
    // Tạo accessToken với privateKey
    const accessToken = await createAccessToken({ userid: userInfo._id, email: userInfo.email }, privateKey);
    return accessToken;
}

B4: Tạo hàm validateToken xử lý xác thực JWT: Lấy publicKey từ DB + Xác thực (verify) token từ publicKey.

// Hàm lấy publicKey
const getPublicKey = async(userid) => {
    const filter = {userid: userid};
    const token = await keytokenModel.findOne(filter);
    return token.publicKey;
}
// Hàm validateToken. Xử lý 2 việc: Lấy publicKey từ DB + Xác thực (verify) token từ publicKey
const validateToken = async(accessToken, userID) => {
    // Lấy publicKey trong DB
    const publicKeyString = await getPublicKey(userID);
    // Convert publicKey từ dạng string về dạng rsa có thể đọc được
    const publicKeyObject = crypto.createPublicKey(publicKeyString);
    // xác thực accessToken sử dụng publicKey
    jwt.verify(accessToken, publicKeyObject, (err, decode) => {
        if(err) {
            console.error('error verify token');
        } else{
            console.log('decode jwt::', decode);
        }
    });
}

B5: Kiểm tra kết quả

// Thực thi
async function runScript() {
    const userId = 1;
    let accessToken = await genToken({_id: userId, email: 'pdthien@gmail.com'});
    await validateToken(accessToken, userId);
};
  • Nhược điểm: Phải lưu thêm Public key vào DB và truy xuất tới DB để lấy Public key khi cần xác thực, dẫn tới hiệu năng có thể bị ảnh hưởng. Tuy nhiên, ta hoàn toàn có thể cải thiện hiệu năng bằng cách sử dụng Cache như MemoryCache hoặc RedisCache.
  • Ưu điểm:
    • Tuy làm gia tăng áp lực cho server khi phải lưu thêm Public key vào DB và lấy ra sử dụng khi cần xác thực, nhưng nó giúp cho hệ thống của chúng ta gia tăng bảo mật hơn rất nhiều.
      (Vì như mình đã nói ở trên, dù hacker có đọc được thông tin Public key từ DB, cũng không thể tạo được fake JWT để truy cập ứng dụng do Private key chỉ được dùng để mã hóa (sign) thông tin tạo JWT, còn Public key chỉ được dùng để giải mã JWT để lấy thông tin, không có chiều ngược lại)
    • Giải quyết được một số nhược điểm của JWT như phần 1 mình đã chỉ ra cho anh em:
      1. Về vấn đề về Rủi ro bảo mật, thì khi áp dụng phương pháp mã hóa bất đối xứng, ta hoàn toàn có thể yên tâm vì độ bảo mật đã được nâng cao hơn rất nhiều (mình đã giải thích ở trên).
      2. Đối với vấn đề Không thể hủy bỏ token và Không hỗ trợ quản lý phiên, thì khi áp dụng phương pháp này, do chúng đã ta lưu publicKey trong DB, nên để hủy bỏ token hay hủy phiên đăng nhập của người dùng một cách đơn giản, ta chỉ việc xóa publicKey đó khỏi DB.

Note: Có nhiều anh em thắc mắc rằng nhỡ hacker lấy được privateKey thì sao? Thì anh em cứ yên tâm nhé, vì không có chuyện đó xảy ra đâu, do privateKey mình chỉ dùng sign tạo JWT, sau đó nó hoàn toàn biến mất khỏi hệ thống nên hacker không bao giờ có thể tìm được nhé hehe.

Anh em tham khảo source code GIT ở đây nhé.

Kết

Tổng kết lại một chút, khi anh em áp dụng mã hoá đối xứng vào JWT thì sẽ dễ thực hiện hơn và server không cần phải lưu thêm thông tin gì cho việc xác thực, qua đó làm tăng hiệu năng, tuy nhiên lại giảm bảo mật vì hacker chỉ cần lấy được sercetKey là có thể fake JWT để truy cập hệ thống. Còn khi áp dụng mã hoá bất đối xứng, ta cần phải lưu thêm publicKey vào DB, tuy có thể làm giảm hiệu năng một chút nhưng bù lại nó giúp ta gia tăng bảo mật hơn rất nhiều.

Nếu anh em có góp ý hay thảo luận thì comment ngay dưới bài viết nhé. Tks all !!!


Nguồn tham khảo:

  1. https://www.youtube.com/watch?v=pK3oBX0vB38

All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.