LoginSignup
4
2

More than 1 year has passed since last update.

【TypeScprit×Node.js(Express.js)】によるJWTトークンを復号しユーザーを取得する

Posted at

はじめに

今回は、TypeScriptによるJWTトークンの複合処理と、復号したトークンでユーザーを取得する方法について、久方ぶりに手こずったので、備忘録として残します。

Userモデル

まず、前提としてユーザーのデータは以下のようになっています。

user.ts
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の実装

データの概要を見たところで、早速本題に入ります。

auth.ts
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に記述する必要がなくなるという仕組みです。

index.ts
// エンドポイントからAPIを呼び出す
app.use("/api/v1", require("./src/v1/routes/auth"));

上記の場合は、/api/v1/各APIで設定したエンドポイントでAPIを呼び出すことができます。

本筋に戻りますと、第二引数のvefiryTokenがミドルウェアとなっており、ユーザーが認証済みかどうかをユーザーを取得することで確認する処理を記述しています。

このミドルウェアによる検証が正常終了した時に、第三引数の認証済みのユーザー情報が返却される仕組みになっています。

それでは、次にミドルウェアの実装を見てみましょう。

middleware

コードとしては、以下のような実装にしています。

tokenHandler.ts
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で半角スペースで分割していますが、これは半角スペースの後ろ部分にトークンがあるため、このようにしています。

続いて、復号処理ですが、これはjsonwebtokenverify関数で行えます。
第一引数に、復号前のトークンを渡し、第二引数に環境変数としている秘密鍵を渡します。
環境変数ですが、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' に存在しません

これはdecodedTokenstringJwtPayloadの2つの型となるため、string型のユーザーIDの検索に使用できないためエラーとなります。
したがって、正常コードのようにas anyとすることで、ユーザーIDとしユーザーを取得するようにしています。

おわりに

JavaScriptと比べるとコードにさまざまな工夫が必要となるため、大変かと思いますが、どなたかの参考になれば幸いです。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2