はじめに
こちらはNode.jsで使用できるライブラリjsonwebtoken
を使ってシンプルな認証機能を作成する記事です。
先日私が書いたJWTトークンに関する以下の記事を実際にコードで具体化したものになります。
もしよろしければ下記の記事も読んでいただけると幸いです
では環境構築から始めていきます。
環境構築
今回はNext.js(13.4.10)環境で認証機能を作成していきます。
- Windows11
- Next.js(13.4.10)
npx create-next-app jsonwebtoken-sample
Next.jsの環境が構築できたらjsonwebtoken
と型定義の@types/jsonwebtoken
をインストールします。
npm install jsonwebtoken @types/jsonwebtoken
ログインページとAPIを作成
次にログインページにログイン認証API、ユーザー情報をクレームに含んだJWTを検証するAPIを作成します。
ディレクトリ構成
ログインページ
本来はロジックやJSXをファイル分割すべきだと思うのですが、今回はデフォルトで設置されているsrc/app/page.tsx
ファイルに全て記述しました。
"use client";
import { FormEvent, MouseEvent, useState } from "react";
type Result = {
user: {
userId: number;
username: string;
};
message: string;
};
export default function Home() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
// 1⃣ デフォルト値としてuserId: 9999とusername: ゲストユーザー を設定。
const [user, setUser] = useState({
userId: 9999,
username: "ゲストユーザー",
});
// 2⃣ emailとpasswordを使ったログイン認証API
const login = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const payload = {
email,
password,
};
try {
const response = await fetch("/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
const { user, message }: Result = await response.json();
if (response.status === 401) {
window.alert(message);
} else if (response.status === 200) {
setUser(user);
window.alert(message);
}
} catch (err) {
window.alert(`エラーが発生しました。: ${err}`);
}
setEmail("");
setPassword("");
};
// 3⃣ JWTトークンを検証するAPIにGETリクエストを送信する関数
const verifyJwt = async (
e: MouseEvent<HTMLButtonElement, globalThis.MouseEvent>
) => {
e.preventDefault();
try {
const response = await fetch("/api/verify-jwt");
const { user, message }: Result = await response.json();
if (response.status === 401) {
window.alert(message);
} else if (response.status === 200) {
setUser(user);
window.alert(message);
}
} catch (err) {
window.alert(`エラーが発生しました。: ${err}`);
}
};
// 4⃣ ログインページ
return (
<main>
<div className="flex flex-col justify-around items-center h-screen">
<div>
<p className="mb-4">ユーザーID: {user.userId}</p>
<p>ユーザー名: {user.username}</p>
<button
onClick={(e) => verifyJwt(e)}
className="px-4 py-2 mt-4 bg-indigo-400 rounded text-white"
>
ユーザー情報確認
</button>
</div>
<div className="flex justify-center items-center h-96 w-96 bg-gray-100">
<form onSubmit={(e) => login(e)}>
<div className="flex flex-col justify-center items-center">
<label htmlFor="email">email</label>
<input
type="email"
id="email"
placeholder="input email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mb-8 py-2 px-4"
/>
<label htmlFor="password">password</label>
<input
type="password"
id="password"
placeholder="input password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mb-8 py-2 px-4"
/>
<button
type="submit"
className="bg-indigo-500 py-2 px-4 rounded text-white"
>
送信
</button>
</div>
</form>
</div>
</div>
</main>
);
}
- ①はログイン認証を行っていないユーザーを想定してユーザーIDを
9999
、ユーザー名をゲストユーザー
としてデフォルト値を設定しています。 - ②はフォーム画面で入力したemailとpasswordを
src/app/api/login
にログイン認証APIにPOSTで送信する関数を定義しています。 - ③はJWTの妥当性を検証するAPI
src/app/api/verify-jwt
に対して、cookieに保存されているユーザー情報を含んだJWTトークンをGETで送信する関数を定義しています。 - ④はフォーム入力欄や、現在のユーザー情報を表示する要素を含んだログインページです。
ログイン認証API
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
import jwt from "jsonwebtoken";
export async function POST(request: Request) {
const { email, password } = await request.json();
/* 1⃣ 本来はDBにemailでユーザーが存在するかを確認して、ハッシュ化されたパスワードと比較を行うと思うのですが、
今回はシンプルなログイン認証としたいので、emailとpasswordをハードコーディングしています。
*/
if (email === "hirohana@co.jp" && password === "1234") {
const user = {
userId: 1,
username: "hirohana",
};
// 2⃣ cookieにJWTを保存する。
const secretKey = process.env.SECRET_KEY as string;
const token = jwt.sign(user, secretKey, {
expiresIn: 120,
algorithm: "HS256",
});
cookies().set({
name: "token",
value: token,
httpOnly: true,
secure: true,
sameSite: "strict",
path: "/",
});
// 3⃣ ユーザーにuserとmessageを格納したオブジェクトを返却する
const message = "ログイン認証に成功しました。";
return NextResponse.json({ user, message }, { status: 200 });
} else {
// 4⃣ ログイン認証に失敗した場合は下記のmessageを格納したオブジェクトを返却する。
const message = "emailまたはpasswordが違っています。";
return NextResponse.json({ message }, { status: 401 });
}
}
- ①は本来、フォームから送信されたemailとpasswordを使ってデータベースに問い合わせを行い、妥当性を検証する必要があるのですが、今回はシンプルなログイン認証機能という事でemailは
hirohana@co.jp
、passwordは1234
とハードコーディングをしています。 - ②は
jwt.sign
関数を呼び出し、引数にユーザー情報と.envファイルに格納されているシークレットキーを渡してJWTトークンを作成しcookieに保存しています。 - ③ですが、ログイン認証に成功した場合はユーザー情報(userId、username)を格納した
user
変数と、ログイン認証に成功した
というメッセージを格納したmessage変数をjsonとして返却しています。 - ④はログイン認証に失敗した場合に
emailまたはpasswordが違っています
という文言を格納したmessage変数をjsonとして返却しています。
JWT検証API
import { cookies } from "next/headers";
import jwt from "jsonwebtoken";
import { NextResponse } from "next/server";
export async function GET() {
// 1⃣ クッキーに保存されたtokenという名前のJWTを取得。
const object = cookies().get("token") ?? { value: "" };
const token = object.value;
const secretKey = process.env.SECRET_KEY as string;
try {
// 2⃣ cookieから取得したtokenと、環境変数に格納しているsecretKeyを使い
// tokenが改ざんされていないか、有効期限は切れていないかをverify関数を使って検証している。
const user = jwt.verify(token, secretKey);
// 3⃣ tokenに問題が無ければtokenから取得したuser情報とmessageをレスポンスとして返却。
const message = "ユーザー情報を取得できました。";
return NextResponse.json({ user, message }, { status: 200 });
} catch (err) {
// 4⃣ tokenが改ざんされていたり、有効期限が切れていたら下記のmessageをレスポンスとして返却。
const message = `ユーザー情報を取得できませんでした。 ${err}`;
return NextResponse.json({ message }, { status: 401 });
}
- ①はクッキーに保存されている
token
という名称のユーザー情報(userId、username)が格納されたJWTトークンを取得しています。 - ②はクッキーから取得したJWTトークンと環境変数に格納しているシークレットキーを使い、トークンが改ざんされていないか、有効期限は切れていないかを
jwt.verify
関数を使って検証しています。 - ③はJWTトークンの検証に問題が無ければトークンから取得したユーザー情報(userId、username)と
ユーザー情報を取得できた
というメッセージをjsonとして返却しています。 - ④はJWTトークンが改ざんされていたり有効期限が切れていた場合はエラーハンドリングされcatch句に飛び、
ユーザー情報が取得できなかった
というメッセージをjsonとして返却しています。
シークレットキーを.envファイルに記述
ログイン認証API、JWT検証APIで使用するシークレットキーを.envファイルに記述します。
SECRET_KEY=secretKey
ログイン認証APIの確認
ログイン認証APIを確認していきます。
emailをhirohana@co.jp
、passwordを1234
で入力し、ログイン認証APIに送信するとページ上部のユーザーID
とユーザー名
が更新されていることが分かります。
またChormeの開発者ツールのApplicationのタブのcookieを見ると、JWTトークンが格納されていることが確認できます。
念のためJWTトークンとして認識されているか https://jwt.io/ のサイトにて確認を行ったところ、上図を見たら分かるようにデコードされたuserIdとusernameが表示されていました。
JWT検証APIの確認
次にcookieに保存されているJWTトークンを検証するためJWT検証APIにGETリクエストを送信し、ペイロードからuserIdとusernameがレスポンスとして渡ってきて表示されるか確認を行います。
一度画面をリロードするとログインページ上部のユーザーIDとユーザー名は、デフォルト値に戻るのが確認できると思います。その後でユーザー情報確認のボタンを押せばJWTトークン検証APIにリクエストが送信され、再度ユーザーID及びユーザー名が更新されます。
もしJWTトークンの有効期限が切れた場合は、window.alert
にて期限切れエラーメッセージが下図のように表示されます。今回のコードではjwt.sign
関数の中でexpiresIn: 120
で設定していますので、ログイン認証の2分後にユーザー情報確認ボタンを押せばJWTトークンの期限が切れることになります。
ペイロードに個人情報を格納してはいけない
JWTトークンのヘッダーとペイロードはBase64URLエンコードされているだけで、暗号化されているわけではありません。したがって万が一JWTトークンが盗まれた際に、Base64URLデコードされてしまうと容易に個人情報が盗まれますので、ペイロードに含める情報は仮に流出しても問題ない情報ユーザーID
やユーザー名
、ユーザーのプロフィール写真URL
などしか格納してはいけないことに注意が必要です。
おわりに
最後まで記事をご覧いただきありがとうございました。
間違い等ありましたらご指摘いただけると幸いです
参考