Passport.jsとJWTを使用してExpress.js REST APIにロールベースのアクセス制御を実装する方法

ロールベースのアクセス制御とは、安全な認証メカニズムです。これを使用すると、特定の役割を持つユーザーに特定のリソースへのアクセスを制限できます。

このタイプの認証は、システム管理者がユーザーの指定された役割に従って権限を制御するのに役立ちます。このレベルのきめ細かい制御により、セキュリティのレイヤーが追加され、アプリは不正アクセスを防ぐことができます。

Passport.jsとJWTを使用してロールベースのアクセス制御メカニズムを実装する

ロールベースのアクセス制御(RBAC)は、ユーザーの役割と権限に基づいてアプリケーションのアクセス制限を適用するために使用される一般的なメカニズムです。RBACメカニズムを実装するためのさまざまな方法があります。

2つの一般的なアプローチには、AcessControlのような専用のRBACライブラリを使用するか、既存の認証ライブラリを活用してメカニズムを実装することが含まれます。

この場合、JSON Webトークン(JWT)は認証資格情報を送信するための安全な方法を提供し、Passport.jsは柔軟な認証ミドルウェアを提供することで認証プロセスを簡素化します。

このアプローチを使用すると、ユーザーに役割を割り当て、認証時にJWTでエンコードできます。その後、JWTを使用して、後続のリクエストでユーザーのIDと役割を検証し、ロールベースの認可とアクセス制御を可能にします。

どちらのアプローチにも利点があり、RBACの実装に効果的です。どの方法を実装するかを選択する際には、プロジェクトの特定の要件に応じて決める必要があります。

このプロジェクトのコードは、GitHubリポジトリからダウンロードできます。

Express.jsプロジェクトのセットアップ

開始するには、ローカルにExpress.jsプロジェクトをセットアップします。プロジェクトをセットアップしたら、先に進んで、次のパッケージをインストールします。

npm install cors dotenv mongoose cookie-parser jsonwebtoken mongodb \
passport passport-local

次に、MongoDBデータベースを作成するか、MongoDB Atlasでクラスタをセットアップします。データベース接続URIをコピーして、プロジェクトのルートディレクトリにある.envファイルに追加します。

CONNECTION_URI="connection URI"

データベース接続の設定

ルートディレクトリで、新しいutils/db.jsファイルを作成し、Mongooseを使用してAtlasで実行されているMongoDBクラスタへの接続を確立する以下のコードを追加します。

const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.CONNECTION_URI);
console.log("Connected to MongoDB!");
} catch (error) {
console.error("Error connecting to MongoDB:", error);
}
};
module.exports = connectDB;

データモデルの定義

ルートディレクトリで、新しいmodel/user.model.jsファイルを作成し、Mongooseを使用してユーザーデータのデータモデルを定義する以下のコードを追加します。

const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: String,
password: String,
role: String
});
module.exports = mongoose.model('User', userSchema);

APIエンドポイントのコントローラーの作成

ルートディレクトリに新しいcontrollers/user.controller.jsファイルを作成し、以下のコードを追加します。

最初に、これらのインポートを行います。

const User = require('../models/user.model');
const passport = require('passport');
const { generateToken } = require('../middleware/auth');
require('../middleware/passport')(passport);

次に、ユーザー登録とログイン機能を管理するロジックを定義します。

exports.registerUser = async (req, res) => {
const { username, password, role } = req.body;
try {
await User.create({ username, password, role });
res.status(201).json({ message: 'User registered successfully' });
} catch (error) {
console.log(error);
res.status(500).json({ message: 'An error occurred!' });
}
};
exports.loginUser = (req, res, next) => {
passport.authenticate('local', { session: false }, (err, user, info) => {
if (err) {
console.log(err);
return res.status(500).json({
message: 'An error occurred while logging in'
});
}
if (!user) {
return res.status(401).json({
message: 'Invalid login credentials'
});
}
req.login(user, { session: false }, (err) => {
if (err) {
console.log(err);
return res.status(500).json({
message: 'An error occurred while logging in'
});
}
const { _id, username, role } = user;
const payload = { userId: _id, username, role };
const token = generateToken(payload);
res.cookie('token', token, { httpOnly: true });
return res.status(200).json({ message: 'Login successful' });
});
})(req, res, next);
};

registerUser関数は、リクエストボディからユーザー名、パスワード、役割を抽出することで、新規ユーザーの登録を処理します。次に、データベースに新しいユーザーエントリを作成し、プロセス中にエラーが発生した場合は成功メッセージまたはエラーに応答します。

一方、loginUser関数は、Passport.jsが提供するローカル認証戦略を利用することで、ユーザーのログインを容易にします。ユーザーの資格情報を認証し、ログインに成功するとトークンを返し、そのトークンは後続の認証済みリクエストのためにCookieに保存されます。ログイン処理中にエラーが発生した場合は、適切なメッセージを返します。

最後に、データベースからすべてのユーザーのデータを取得するロジックを実装するコードを追加します。このエンドポイントは制限付きルートとして使用し、adminの役割を持つ権限のあるユーザーのみがこのエンドポイントにアクセスできるようにします。

exports.getUsers = async (req, res) => {
try {
const users = await User.find({});
res.json(users);
} catch (error) {
console.log(error);
res.status(500).json({ message: 'An error occurred!' });
}
};

Passport.jsローカル認証戦略のセットアップ

ユーザーがログイン資格情報を提供した後にユーザーを認証するには、ローカル認証戦略をセットアップする必要があります。

ルートディレクトリに新しいmiddleware/passport.jsファイルを作成し、次のコードを追加します。

const LocalStrategy = require('passport-local').Strategy;
const User = require('../models/user.model');
module.exports = (passport) => {
passport.use(
new LocalStrategy(async (username, password, done) => {
try {
const user = await User.findOne({ username });
if (!user) {
return done(null, false);
}
if (user.password !== password) {
return done(null, false);
}
return done(null, user);
} catch (error) {
return done(error);
}
})
);
};

このコードは、ユーザーが提供したユーザー名とパスワードに基づいてユーザーを認証するためのローカルpassport.js戦略を定義します。

最初に、一致するユーザー名を持つユーザーを検索するためにデータベースにクエリを発行し、次にパスワードを検証します。その結果、ログインプロセスが成功した場合は、認証されたユーザーオブジェクトを返します。

JWT検証ミドルウェアの作成

middlewareディレクトリ内で、新しいauth.jsファイルを作成し、JWTを生成して検証するミドルウェアを定義するために次のコードを追加します。

const jwt = require('jsonwebtoken');
const secretKey = process.env.SECRET_KEY;
const generateToken = (payload) => {
const token = jwt.sign(payload, secretKey, { expiresIn: '1h' });
return token;
};
const verifyToken = (requiredRole) => (req, res, next) => {
const token = req.cookies.token;
if (!token) {
return res.status(401).json({ message: 'No token provided' });
}
jwt.verify(token, secretKey, (err, decoded) => {
if (err) {
return res.status(401).json({ message: 'Invalid token' });
}
req.userId = decoded.userId;
if (decoded.role !== requiredRole) {
return res.status(403).json({
message: 'You do not have the authorization and permissions to access this resource.'
});
}
next();
});
};
module.exports = { generateToken, verifyToken };

generateToken関数は指定された有効期限を持つJWTを作成し、verifyToken関数はトークンが存在して有効であるかどうかをチェックします。さらに、デコードされたトークンに必須の役割が含まれているかどうかも検証します。これは、基本的に、許可された役割と権限を持つユーザーのみがこのリソースにアクセスできるようにします。

JWTに一意に署名するには、一意の秘密鍵を生成して、以下のように.envファイルに追加する必要があります。

SECRET_KEY="This is a sample secret key."

APIルートの定義

ルートディレクトリで、新しいフォルダを作成してroutesという名前を付けます。このフォルダ内で、新しいuserRoutes.jsを作成し、次のコードを追加します。

const express = require('express');
const router = express.Router();
const userControllers = require('../controllers/userController');
const { verifyToken } = require('../middleware/auth');
router.post('/api/register', userControllers.registerUser);
router.post('/api/login', userControllers.loginUser);
router.get('/api/users', verifyToken('admin'), userControllers.getUsers);
module.exports = router;

このコードは、REST APIのHTTPルートを定義します。特にusersルートは保護されたルートとして機能します。adminロールを持つユーザーへのアクセスを制限することで、ロールベースのアクセス制御を効果的に適用します。

メインサーバーファイルの更新

server.jsファイルを開いて、次のように更新します。

const express = require('express');const cors = require('cors');const cookieParser = require('cookie-parser');const app = express();const port = 5000;require('dotenv').config();const connectDB = require('./utils/db');const passport = require('passport');require('./middleware/passport')(passport);connectDB();app.use(express.json());app.use(express.urlencoded({ extended: true }));app.use(cors());app.use(cookieParser());app.use(passport.initialize());const userRoutes = require('./routes/userRoutes');app.use('/', userRoutes);app.listen(port, () => {console.log(Server is running on port ${port});});

最後に、開発サーバーを起動してアプリケーションを実行します。

node server.js

RBACメカニズムを活用して認証システムを向上させる

ロールベースのアクセス制御を実装することは、アプリケーションのセキュリティを強化する効果的な方法です。

既存の認証ライブラリを組み込んで効率的なRBACシステムを確立することは優れたアプローチですが、RBACライブラリを活用してユーザーの役割を明確に定義し、権限を割り当てることで、さらに堅牢なソリューションを提供し、最終的にはアプリケーションの全体的なセキュリティを強化できます。