概要
- express – 4.18.2
- crypto.js – 3.0.0
- jsonwebtoken – 9.0.0
- mongoose – 6.9.0
- dotenv – 16.0.3
- express-validator – 6.14.3
インプルの藤谷です。
expressを利用してユーザー新規登録、ログインとJWTの検証機能までを解説します。
JWTのリフレッシュや、passport-jwtでのtoken検証、TypeScriptの利用はせずに、簡易なものに留めます。
実務レベルではなく、流れを掴む程度のイメージで見て頂けると幸いです。
解説
server起動
expressインスタンスを作成し、3000番portをnpm startで起動します。
mongoDBを利用する為、ORMにはmongooseを使います。
pathは/api/v1をbaseURLとし、require内のディレクトリ配下に格納されたauth.jsをrouteとして見に行きます。
// app.ts
const express = require("express");
const mongoose = require("mongoose");
const app = express()
const port = 3000;
require("dotenv").config();
app.use(express.json());
app.use("/api/v1", require("./src/v1/routes/auth"));
mongoose.set("strictQuery", true);
try {
mongoose.connect(process.env.MONGODB_URL);
console.log("db接続正常");
} catch (error) {
console.log("error");
}
app.listen(port, () => {
console.log("local server started");
});
mongoDBのschemaは以下の定義です。
// schema.js
const mongoose = require("mongoose");
const userSchema = new mongoose.Schema({
username: {
type: String,
require: true,
unique: true,
},
password: {
type: String,
require: true,
},
});
module.exports = mongoose.model("User", userSchema);
Route
routerオブジェクトでエンドポイントを指定し、バリデーション検証、バリデーション失敗時にエラーハンドリング、成功時のメソッド実行を呼び出しています。
verifyTokenメソッドに関しては、middlewareでtoken検証用メソッドを実行し、通ればresponseとしてstatus code 200と、json形式のuserを返却させています。
// routers/auth.js
const router = require("express").Router();
const validation = require("../validate/validateError");
const { registerValidator, loginValidator } = require("../validate/auth");
const userController = require("../controllers/auth");
const tokenHandler = require("../middleware/jwtVerify");
router.post(
"/register",
verifyToken
registerValidator,
validation.validateError,
userController.register
);
router.post(
"/login",
verifyToken
loginValidator,
validation.validateError,
userController.login
);
module.exports = router;
express-validator
expressのバリデーションライブラリであるexpress-validatorを利用してカラムの検証条件を定義します。
usernameに関しては「既に同じユーザー名が存在していれば」の、カスタムバリデーションを定義します。
エンドポイント毎に定義を分けてexportし、routeから呼び出しています。
// validate/auth.js
const { body } = require("express-validator");
const User = require("../models/user");
exports.registerValidator = [
body("username")
.isLength({ min: 6 })
.withMessage("user name must be at least 6 characters long")
.not(),
body("username").custom((value) => {
return User.findOne({ username: value }).then((user) => {
if (user) {
return Promise.reject("this username is already registered.");
}
});
}),
body("password")
.isLength({ min: 6 })
.withMessage("password must be at least 6 characters")
.not(),
body("confirmPassword")
.isLength({ min: 6 })
.withMessage("confirmation password must be at least 6 characters")
.not(),
];
exports.loginValidator = [
body("username")
.isLength({ min: 6 })
.withMessage("user name must be at least 6 characters long")
.not(),
body("password")
.isLength({ min: 6 })
.withMessage("password must be at least 6 characters")
.not(),
];
エラーハンドリングは共通処理とし、errors.isEmpty()が論理否定で記述され、errorsオブジェクトが空でなければ400の処理とerrorsのjsonが返却されます。
// validate/validateError.js
const { validationResult } = require("express-validator");
exports.validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
};
メソッド定義 register / login
各エンドポイントのバリデーション成功時に実行されるメソッドがまとまっていますので、個別に解説していきます。
// controller/auth.js
const CryptoJS = require("crypto-js");
const JWT = require("jsonwebtoken");
const User = require("../models/user");
exports.register = async (req, res) => {
const password = req.body.password;
try {
req.body.password = CryptoJS.AES.encrypt(password, process.env.SECRET_KEY);
const user = await User.create(req.body);
return res.status(200).json({ user });
} catch (err) {
return res.status(500).json(err);
}
};
exports.login = async (req, res) => {
const { username, password } = req.body;
try {
user = await User.findOne({ username });
if (!user) {
res.status(401).json({
errors: {
params: "username",
message: "invalid user name.",
},
});
}
const decryptedPassword = CryptoJS.AES.decrypt(
user.password,
process.env.SECRET_KEY
).toString(CryptoJS.enc.Utf8);
if (decryptedPassword !== password) {
res.status(401).json({
errors: {
params: "password",
message: "invalid password.",
},
});
}
const token = JWT.sign({ id: user._id }, process.env.TOKEN_SECRET_KEY, {
expiresIn: "24h",
});
return res.status(201).json({ user, token });
} catch (err) {
return res.status(500).json(err);
}
};
register
ユーザー新規登録APIです。
入力されたpasswordをCrypto.jsを用いてencode(暗号化)し、User.createでユーザーを作成します。
今回の暗号化方式は共通暗号鍵方式になるので、encodeとdecodeには同じenvの鍵を指定しています。
ここまで成功であれば、status 200と共に、userとtokenをjson形式で返却します。
try catchで記述しているので、処理が落ちればserver errorの500とerrorオブジェクトが返却されます。
login
req.bodyをusernameとpasswordに分割代入し、findOneでusernameを一つ検索します。存在しなければclient error(認証系)の401とerror messageを返却します。
usernameが存在すれば、passwordの検索に移ります。
Cryotp.jsで暗号化されたpasswordをdecodeし、toStringでキャストする事でpasswordを復号します。
復号されたpasswordと入力値のpasswordが一致すれば、jwt tokenの発行に移ります。エラー時はusernameと同様です。
最後に、JWT.signでjwt tokenを生成します。payloadに入るデータには暗号化されたidを利用します。tokenの有効期限は24時間です。
全て成功すれば、userとtokenをjson形式で201と共に返却します。
処理が落ちればcatchでエラーハンドリングが走ります。
メソッド定義 verifyToken
verifyToken
// middleware/jwtVerify.js
const JWT = require("jsonwebtoken");
const User = require("../models/user");
const tokenDecode = (req) => {
const bearerHeader = req.headers.authorization;
if (bearerHeader) {
const bearer = bearerHeader.split(" ")[1];
try {
const decodedToken = JWT.verify(bearer, process.env.TOKEN_SECRET_KEY);
return decodedToken;
} catch {
return false;
}
} else {
return false;
}
};
exports.verifyToken = async (req, res, next) => {
const tokenDecoded = tokenDecode(req);
if (tokenDecoded) {
const user = await User.findById(tokenDecoded.id);
if (!user) {
return res.status(401).json("not authorized.");
}
req.user = user;
next();
} else {
return res.status(401).json("not authorized.");
}
};
jwt tokenの検証用メソッドを実装します。
verifyTokenの実行で、先ずはtokenのdecodeを行います。
tokenDecodeメソッドをreqオブジェクトを引数に実行します。
request headerのauthorizationにtokenが含まれていることを前提に、tokenを取り出してbearerHeaderに格納します。
格納できた場合、splitメソッドを用いてbearerとtoken部分を切り分けて一つの配列に格納し、index1の指定でtokenのみをbearerに格納します。
成功すれば、JWT.verifyでtokenの有効性を検証し、有効であればdecodeしてdecodedTokenに格納し、返却します。
verifyTokenに処理が戻り、tokenDecodedが存在すれば、findByIdでdecode済みのtokenから取得したidと、Userモデル内レコードのidを比較し、存在すればreq.userで返却。しなければ401と共に「権限がありません」を返却します。
以上で、loginとjwt検証が完了したはずです。
今回は以上となります。
ここまで読んでいただきありがとうございます。