【TypeScprit×Node.js(Express.js)】によるJWTトークンを復号しユーザーを取得する
はじめに
今回は、TypeScriptによるJWTトークンの複合処理と、復号したトークンでユーザーを取得する方法について、久方ぶりに手こずったので、備忘録として残します。
Userモデル
まず、前提としてユーザーのデータは以下のようになっています。
import mongoose from "mongoose";
// userModel作成
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
});
module.exports = mongoose.model("User", userSchema);
ライブラリはmongoose
を使用しています。
また、MongoDB
では以下の項目を持つようにしています。
- _id
- username
- password
- __v
以上が、ユーザーの持っている情報となります。
JWTトークンによるユーザー取得APIの実装
データの概要を見たところで、早速本題に入ります。
import express from "express";
import { verifyToken } from "../middleware/tokenHandler";
const router = express.Router();
// JWT認証APIを呼び出し
router.post(
"/verify-token",
verifyToken,
(req: express.Request, res: express.Response) => {
return res.status(200).json({ user: req.user });
}
);
少し本筋とは外れますが、念のため解説すると、ExpressAppでは、router
という仕組みによりindex.ts
のエンドポイントさえ指定していれば、API側でルーティングの設定ができる仕組みを提供しています。
具体的には、index.ts
で以下のコードを書くだけで、エンドポイントをrouter.HTTPリクエスト()
の引数で設定するだけで良くなり、一つ一つのAPIをindex.ts
に記述する必要がなくなるという仕組みです。
// エンドポイントからAPIを呼び出す
app.use("/api/v1", require("./src/v1/routes/auth"));
上記の場合は、/api/v1/各APIで設定したエンドポイントでAPIを呼び出すことができます。
本筋に戻りますと、第二引数のvefiryToken
がミドルウェアとなっており、ユーザーが認証済みかどうかをユーザーを取得することで確認する処理を記述しています。
このミドルウェアによる検証が正常終了した時に、第三引数の認証済みのユーザー情報が返却される仕組みになっています。
それでは、次にミドルウェアの実装を見てみましょう。
middleware
コードとしては、以下のような実装にしています。
import express, { NextFunction } from "express";
import jwt from "jsonwebtoken";
const User = require("../models/user");
// express.Requestに拡張でuser型を追加
declare global {
namespace Express {
interface Request {
user?: { id: string };
}
}
}
// JWTトークンを復号する処理
const tokenDecode = (req: express.Request) => {
// リクエストヘッダーの"authorization"を取得
const bearerHeader = req.headers.authorization;
// 認証情報が存在する場合
if (bearerHeader) {
// トークンを取得
const bearer = bearerHeader.split(" ")[1];
try {
// トークンを復号
const decodedToken = jwt.verify(
bearer,
process.env.TOKEN_SECRET_KEY as string
);
return decodedToken;
} catch {
return false;
}
} else {
return false;
}
};
// JWTを検証するためのミドルウェア
const verifyToken = async (
req: express.Request,
res: express.Response,
next: NextFunction
) => {
// 復号したトークンを取得
const decodedToken = tokenDecode(req);
// トークンが存在する場合
if (decodedToken) {
// ユーザーを取得(トークンはもともとユーザーのIDから生成したものであるため検索可能)
const user = await User.findById((decodedToken as any).id);
// ユーザーが存在しない場合
if (!user) {
return res.status(401).json("権限がありません");
}
// リクエスト情報を取得したユーザーで上書き
req.user = user;
next();
} else {
return res.status(401).json("権限がありません");
}
};
export { verifyToken };
まず、疑問になるのは、declare global{}
の部分だと思います。
// express.Requestに拡張でuser型を追加
declare global {
namespace Express {
interface Request {
user?: { id: string };
}
}
}
これは、express.Request型
にuser
をプロパティとして追加するための記述をしています。
これがないと、req.user
という記述で以下のエラーが出力されます。
プロパティ 'user' は型 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>' に存在しません。
ちなみに、このコードはスコープがグローバルで適用されますが、リクエストから取得したいプロパティを選択(ここではuser
)しているファイルと同ファイルに書かないと、エディタ上のエラーはきているのに、上記のエラーが出続けるという事象が発生するので、注意が必要です。
// JWTトークンを復号する処理
const tokenDecode = (req: express.Request) => {
// リクエストヘッダーの"authorization"を取得
const bearerHeader = req.headers.authorization;
// 認証情報が存在する場合
if (bearerHeader) {
// トークンを取得
const bearer = bearerHeader.split(" ")[1];
try {
// トークンを復号
const decodedToken = jwt.verify(
bearer,
process.env.TOKEN_SECRET_KEY as string
);
return decodedToken;
} catch {
return false;
}
} else {
return false;
}
};
次に、JWTトークンを復号する処理を行いますが、JWTトークンは、リクエストヘッダーのauthorization
に入っているため、そちらを取得し、条件分岐させハンドリングしています。
また、トークンの取得時は、split
で半角スペースで分割していますが、これは半角スペースの後ろ部分にトークンがあるため、このようにしています。
続いて、復号処理ですが、これはjsonwebtoken
のverify
関数で行えます。
第一引数に、復号前のトークンを渡し、第二引数に環境変数としている秘密鍵を渡します。
環境変数ですが、as stirng
をつけないと以下、エラーとなります。
型 'undefined' を型 'Secret | GetPublicKeyOrSecret' に割り当てることはできません。
(存在しないユーザーによるアクセスなどがあるため、undefined
の可能性があるので、上記エラーが出ます。)
最後に、認証ユーザーかどうか検証するミドルウェア関数を実装します。
// JWTを検証するためのミドルウェア
const verifyToken = async (
req: express.Request,
res: express.Response,
next: NextFunction
) => {
// 復号したトークンを取得
const decodedToken = tokenDecode(req);
// トークンが存在する場合
if (decodedToken) {
// ユーザーを取得(トークンはもともとユーザーのIDから生成したものであるため検索可能)
const user = await User.findById((decodedToken as any).id);
// ユーザーが存在しない場合
if (!user) {
return res.status(401).json("権限がありません");
}
// リクエスト情報を取得したユーザーで上書き
req.user = user;
next();
} else {
return res.status(401).json("権限がありません");
}
};
ここでのポイントは複合化したトークンでユーザーの取得を行うという点です。
なぜかというと、トークン発行時に、ユーザーIDでトークンを発行しているためです。
以下が、当該コードとなります。
// JWTの発行
const token = jwt.sign(
{ id: user._id },
process.env.TOKEN_SECRET_KEY as string,
{
expiresIn: "24h",
}
);
このため、ユーザーの取得は、findById()
にて、復号化したトークンで取得しているわけです。
ちなみに、findById()
で(decodedToken as any).id
としていますが、これをしないと、これまた以下のようにエラーとなります。
プロパティ 'id' は型 'string | JwtPayload' に存在しません。 プロパティ 'id' は型 'string' に存在しません
これはdecodedToken
はstring
とJwtPayload
の2つの型となるため、string型のユーザーIDの検索に使用できないためエラーとなります。
したがって、正常コードのようにas any
とすることで、ユーザーIDとしユーザーを取得するようにしています。
おわりに
JavaScriptと比べるとコードにさまざまな工夫が必要となるため、大変かと思いますが、どなたかの参考になれば幸いです。
Discussion