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

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

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

```typescript: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の実装
データの概要を見たところで、早速本題に入ります。
```typescript: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`に記述する必要がなくなるという仕組みです。

```typescript:index.ts
// エンドポイントからAPIを呼び出す
app.use("/api/v1", require("./src/v1/routes/auth"));
```
上記の場合は、**/api/v1/各APIで設定したエンドポイント**でAPIを呼び出すことができます。

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

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

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

# middleware
コードとしては、以下のような実装にしています。
```typescript: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{}`の部分だと思います。
```typescript
// 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`)しているファイルと同ファイルに書かないと、エディタ上のエラーはきているのに、上記のエラーが出続けるという事象が発生するので、注意が必要です。

```typescript
// 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`の可能性があるので、上記エラーが出ます。）

最後に、認証ユーザーかどうか検証するミドルウェア関数を実装します。
```typescript
// 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でトークンを発行しているためです。
以下が、当該コードとなります。
```typescript
// 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と比べるとコードにさまざまな工夫が必要となるため、大変かと思いますが、どなたかの参考になれば幸いです。