ログインセッション
CoddyのNode.jsジャーニー「認証とセキュリティ」セクションの一部。レッスン 10/24。
ログインへの応答は200ですが、次のリクエストでは誰がログインしたのか忘れられています。セッションがその情報を記憶します。ログイン時にサーバーは誰にも推測できないIDを作成し、どのユーザーに属するかを保存して、Cookieとして送信します。その後の各リクエストでIDが送り返され、サーバーはそれを検索します。
IDはランダムでなければなりません。ユーザー名を保持するCookieでは、誰でもCookie: sid=adaと入力してAdaになれてしまいます。randomBytes(24)は24個のランダムなバイト、つまり192ビットを生成するため、誰にも推測できません。toString('base64url')はそれらを32文字の英字、数字、-、_で表します。これらはCookieで安全に使える文字です。ページ内のスクリプトが読み取れないよう、CookieにhttpOnlyを設定します。
const cartId = randomBytes(24).toString('base64url');
await carts.insertOne({ _id: cartId, items: [] });
res.cookie('cart', cartId, { httpOnly: true });
// 後続のリクエストで
const cart = await carts.findOne({ _id: parseCookies(req.get('cookie')).cart });チャレンジ
簡単このステップでは、Remembering with Cookiesのプロジェクトファイル(server.jsを含む)ではなく、コース用に用意されたプライベートノートAPIプロジェクトを開きます。以前の作業は、元のレッスンにそのまま残ります。server.jsを編集する前に、用意されているファイルを読んでください。このプロジェクトには、プライベートノート用の登録ルートとログインルートが含まれています。
ログインは機能しますが、次のリクエストでは、誰がログインしたのかをすでに忘れています。セッションを使ってサーバーに記憶させましょう。セッションとは、サーバーに保存され、Cookieとしてブラウザーに送られるランダムなIDです。
server.jsで、node:cryptoからrandomBytesをインポートします。notesとusersの隣に、./db.jsからsessionsをインポートします。ロックされているdb.jsには、すでにsessionsコレクションがあります。POST /loginで、パスワードの確認後にセッションIDを作成します。24個のランダムなバイトをbase64url形式のテキストにします。これは、文字、数字、-、_からなる32文字で、oqjRxvxmXl_nRwAwOLxgZyI2sYq9hZtNのような形式です。ログインのたびに新しいIDを作成します。- セッションを
sessionsに、次の2つのフィールドを持つドキュメントとして保存します。_id:セッションID。userId:ログインしたユーザーの_id。
httpOnlyオプションを指定し、sidという名前のCookieとしてIDをブラウザーに送ります。これにより、ページ内のスクリプトからその値を読み取れなくなります。ログインのレスポンスは引き続き{"username": "ada"}です。GET /meを追加します。このエンドポイントはsidCookieを読み取り、その_idを持つセッションを検索してから、そのセッションのユーザーを検索します。レスポンスは{"username": "ada"}です。sidCookieがない場合、またはそのIDを持つセッションがない場合は、ステータス401と{"error": "Log in first"}を返します。sid=adaのようにCookieを手入力した場合も、401が返されなければなりません。
試してみましょう:「実行」を押します。POST /signupに{"username": "ada", "password": "correct horse"}を送信します。GET /meを送信します。ステータス401と{"error": "Log in first"}が返ります。登録時と同じ本文でPOST /loginを送信します。ステータス200が返ります。レスポンスの「Headers」タブに、ドットの部分がランダムなIDになったSet-Cookie: sid=...; Path=/; HttpOnlyが表示されます。もう一度GET /meを送信します。ブラウザーからCookieが送り返されるので、{"username": "ada"}が返ります。「Data」を開きます。sessionsには1件のドキュメントがあり、_idはあなたのID、userIdはadaの_idになっています。
自分で試してみよう
import express from 'express';
import { parseCookies } from './cookies.js';
import { notes, users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';
const app = express();
app.use(express.json());
app.post('/signup', async (req, res) => {
const username = String(req.body.username ?? '').trim().toLowerCase();
const password = String(req.body.password ?? '');
if (!username || password.length < 8) {
return res.status(400).json({ error: 'Pick a username and a password of at least 8 characters' });
}
if (await users.findOne({ username })) {
return res.status(409).json({ error: 'That username is taken' });
}
await users.insertOne({ username, passwordHash: await hashPassword(password) });
res.status(201).json({ username });
});
app.post('/login', async (req, res) => {
const username = String(req.body.username ?? '').trim().toLowerCase();
const user = await users.findOne({ username });
if (!user || !(await verifyPassword(String(req.body.password ?? ''), user.passwordHash))) {
return res.status(401).json({ error: 'Wrong username or password' });
}
res.json({ username: user.username });
});
// このブラウザが GET /visit を呼び出した回数を数える。
app.get('/visit', (req, res) => {
const cookies = parseCookies(req.get('cookie'));
const visits = Number(cookies.visits ?? 0) + 1;
res.cookie('visits', String(visits));
res.json({ visits });
});
// ノート。現時点では誰でも読み書きできる。
app.get('/notes', async (req, res) => {
res.json(await notes.find().toArray());
});
app.post('/notes', async (req, res) => {
const note = { text: String(req.body.text ?? '') };
await notes.insertOne(note);
res.status(201).json(note);
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});
認証とセキュリティのすべてのレッスン
自分で練習してみよう: Expressプレイグラウンド