研究室専用ログインシステムを作る
これまでHTMLでページを作り、Expressでサーバーを立て、データベースに試料を保存し、Cookieでセッションを管理する方法を学びました。ここまで来ると自然な疑問が生まれます:
「このWebアプリを私たちの研究室メンバーだけが使えるようにできないか?」
試料管理システムに機密性の高い実験データがあるなら、誰でもアクセスできてはいけません。共同研究グループにだけ結果を共有したい場合もあるし、研究発表資料をログインしたレビュアーにだけ見せたい場合もあります。
その時に必要なのがログインシステムです。
Cookieからログインへ
auth-cookiesトピックでCookieとSessionを学びました。「このブラウザはさっきログインした人だ」をサーバーが記憶する仕組みでした。
しかし1つ欠けていたものがあります — 「ログインした人」をどう確認するか? IDとパスワードを受け取り、データベースに保存されているものと比較し、一致すればセッションを作る過程。これを自分で作るとコードが複雑になり、セキュリティのミスが起きやすくなります。
この複雑な認証プロセスを代行してくれるツールがPassport.jsです。
Passport.js:認証専門ミドルウェア
Passport.jsはExpress用の認証ミドルウェアです。「認証はこれを使ってください」と作られたツールです。
npm install passportnpm install passport-local核心概念は**ストラテジー(Strategy)**です。認証方法ごとに個別のストラテジーパッケージがあります:
| ストラテジー | パッケージ | 認証方式 |
|---|---|---|
| Local | passport-local | メール + パスワード |
passport-google-oauth20 | Googleアカウント | |
| GitHub | passport-github2 | GitHubアカウント |
| JWT | passport-jwt | JSON Web Token |
現在Passport.jsには300以上のストラテジーが登録されています。必要な認証方法のストラテジーだけインストールすれば済みます。
Passport初期設定:Expressに組み込む
PassportをExpressに接続する基本設定です:
const express = require("express");
const session = require("express-session");
const passport = require("passport");
const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(session({
secret: "lab-secret-key",
resave: false,
saveUninitialized: false,
}));
app.use(passport.initialize());
app.use(passport.session());passport.initialize() — PassportをExpressに登録します。
passport.session() — セッションに保存されたユーザー情報を毎リクエストごとに復元します。
auth-cookiesトピックで学んだexpress-sessionと一緒に使います。セッションが「記憶」なら、Passportは「誰が記憶されるべきか」を決めます。
serializeUserとdeserializeUser
ユーザーがログインするとセッションにユーザー情報を保存する必要があります。しかしユーザーオブジェクト全体をセッションに入れると重くなります。Passportはこれを**シリアライズ(serialize)/デシリアライズ(deserialize)**パターンで解決します:
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
db.query("SELECT * FROM users WHERE id = ?", [id], function(err, rows) {
done(err, rows[0]);
});
});serializeUser — ログイン成功時に実行。セッションに何を保存するかを決めます。通常user.idのみ保存。
deserializeUser — 毎リクエスト時に実行。保存されたidでデータベースからユーザーを探しreq.userに入れます。
例えると — 図書館の貸出カードに会員番号だけ書いておき(serialize)、後で会員番号で会員情報を照会する(deserialize)のと同じです。
ローカル認証:メール + パスワード
最も基本的なログイン方式です。ユーザーがメールとパスワードを入力すると、データベースで確認してログインを処理します。
const passport = require("passport");
const LocalStrategy = require("passport-local").Strategy;
const bcrypt = require("bcrypt");
passport.use(new LocalStrategy(
{ usernameField: "email" },
function(email, password, done) {
db.query("SELECT * FROM users WHERE email = ?", [email], function(err, rows) {
if (err) return done(err);
if (rows.length === 0) return done(null, false, { message: "未登録のメールアドレス" });
const user = rows[0];
const isMatch = bcrypt.compareSync(password, user.password_hash);
if (!isMatch) return done(null, false, { message: "パスワード不一致" });
return done(null, user);
});
}
));done(null, user) — 認証成功。PassportがこのuserをserializeUserに渡します。
done(null, false) — 認証失敗。エラーではないがログイン拒否。
done(err) — サーバーエラー(DB接続失敗など)。
Passportがやってくれること:
- ログインフォームのメール/パスワードをストラテジーに渡す
- 認証成功時:
serializeUser→ セッションにユーザーID保存 - 以降のリクエストごと:
deserializeUser→req.userで現在のログインユーザー情報にアクセス
以前学んだExpressミドルウェア(app.use())と同じパターンです。Passportもミドルウェアとして組み込めば、ルートでreq.userからログインユーザーを確認できます。
会員登録:新しい研究者の追加
ログインがあるにはまず会員登録が必要です。全体のフロー:
ユーザー → 登録フォーム(メール、パスワード入力)
→ サーバー:メール重複確認
→ サーバー:パスワードをハッシュ処理(bcrypt)
→ サーバー:ハッシュ化されたパスワードをデータベースに保存
→ ログイン可能に実際のExpressルートで実装すると:
const bcrypt = require("bcrypt");
app.post("/register", function(req, res) {
const { email, password, name } = req.body;
// 1. メール重複確認
db.query("SELECT * FROM users WHERE email = ?", [email], function(err, rows) {
if (rows.length > 0) {
return res.status(400).send("既に登録されたメールアドレスです");
}
// 2. パスワードをハッシュ処理
const hashedPassword = bcrypt.hashSync(password, 10);
// 3. データベースに保存
db.query(
"INSERT INTO users (email, password_hash, name, role) VALUES (?, ?, ?, ?)",
[email, hashedPassword, name, "member"],
function(err) {
if (err) return res.status(500).send("登録失敗");
res.redirect("/login");
}
);
});
});パスワードは絶対に平文で保存しません。 bcryptのようなハッシュ関数で変換してから保存します。
const bcrypt = require("bcrypt");
// 会員登録時:平文 → ハッシュ
const hashedPassword = bcrypt.hashSync("myLabPassword", 10);
// → "$2b$10$X7YKz..." のようなハッシュ値を保存
// ログイン時:入力値のハッシュと保存されたハッシュを比較
const isMatch = bcrypt.compareSync("myLabPassword", hashedPassword);
// → true または falsebcrypt.hashSync(password, 10) — 10はソルトラウンド(ハッシュ反復回数)。数が大きいほど安全ですが遅くなります。10が一般的な推奨値です。
これは研究室のセキュリティと同じです。建物の入退室カードのコードが平文で保存されていれば、データベースが流出した時にすべてのドアが開きます。ハッシュ処理は — カードを複製できなくすることです。
ログイン/ログアウトルート
会員登録後の実際のログイン/ログアウトルート:
// ログインページの表示
app.get("/login", function(req, res) {
res.send(`
<form action="/login" method="POST">
<input type="email" name="email" placeholder="メールアドレス" required />
<input type="password" name="password" placeholder="パスワード" required />
<button type="submit">ログイン</button>
</form>
`);
});
// ログイン処理 — PassportがLocalStrategyを自動実行
app.post("/login",
passport.authenticate("local", {
successRedirect: "/dashboard",
failureRedirect: "/login",
})
);
// ログアウト
app.get("/logout", function(req, res) {
req.logout(function(err) {
if (err) return res.status(500).send("ログアウト失敗");
res.redirect("/");
});
});passport.authenticate("local") — ストラテジー名「local」を指定するとPassportがそのストラテジーを実行します。成功すればsuccessRedirect、失敗すればfailureRedirectに移動します。「local」はpassport-localパッケージが登録するデフォルト名です。
アクセス制御:誰が何を見られるか
ログインシステムの本当の目的はアクセス制御です。「このページはログインした人だけ」「この機能は管理者だけ」— これを実装します。
// ログインユーザーのみアクセス可能なミドルウェア
function requireLogin(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect("/login");
}
// 試料データ — ログイン必須
app.get("/samples/confidential", requireLogin, function(req, res) {
res.json({ data: "機密実験データ", user: req.user.email });
});
// 公開ページ — 誰でもアクセス可能
app.get("/publications", function(req, res) {
res.json({ papers: publicPapers });
});バイオ研究ではこのようなシナリオが一般的です:
| シナリオ | アクセス制御 |
|---|---|
| 研究室内部の試料管理 | ラボメンバーのみログイン可能 |
| 共同研究データ共有 | 登録された研究グループのみ閲覧 |
| 研究発表資料 | レビュアーにのみ公開 |
| 機器予約システム | ログインした研究者のみ予約 |
| 結果ダッシュボード | PIは全体、学生は自分のデータのみ |
複数ユーザーと役割(Role)
ユーザーが複数いると**役割(Role)**が必要です:
// ユーザーテーブル構造
// id | email | password_hash | role
// 1 | pi@lab.com | $2b$10$... | admin
// 2 | grad1@lab.com | $2b$10$... | member
// 3 | intern@lab.com | $2b$10$... | viewer
function requireAdmin(req, res, next) {
if (req.isAuthenticated() && req.user.role === "admin") {
return next();
}
res.status(403).json({ error: "管理者権限が必要です" });
}
// 試料削除 — 管理者のみ
app.delete("/sample/:id", requireAdmin, function(req, res) {
// 削除ロジック
});PI(教授)はadmin、大学院生はmember、インターンはviewer — 研究室の権限構造をそのままコードに写し取ります。
OAuth 2.0:ソーシャルログインの仕組み
「Googleアカウントでログイン」ボタンを見たことがあるでしょう。これがOAuth 2.0です。
核心原理を例えで説明すると — ホテルのカードキー発行プロセスと同じです:
1. 宿泊客(ユーザー)がホテル(自分のWebアプリ)にチェックインしたい
2. ホテルは直接身分確認する能力がない
3. ホテル:「あちらのフロントデスク(Google)で身分確認してきてください」
4. 宿泊客がフロントデスクでパスポート(Google ID/パスワード)を提示
5. フロントデスクが仮交換券(Authorization Code)を発行
6. ホテルが交換券 + 事業者登録証(Client Secret)でカードキー(Access Token)と交換
7. カードキーで宿泊客情報(名前、メール)の照会が可能に核心:自分のWebアプリはユーザーのGoogleパスワードを一切見ません。 Googleが代わりに身分を確認し、「この人で間違いありません」というトークンだけを渡します。
OAuthの登場人物3人
| 役割 | 正体 | 例え |
|---|---|---|
| Resource Owner | ユーザー(研究者) | ホテルの宿泊客 |
| Client | 自分のWebアプリ(試料管理システム) | ホテル |
| Resource Server | Google、GitHubなど | フロントデスク(身分確認) |
Passport.jsでGoogle OAuthを使うと:
const GoogleStrategy = require("passport-google-oauth20").Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "/auth/google/callback"
},
function(accessToken, refreshToken, profile, done) {
// profile.emails[0].value → ユーザーメール
// データベースで探すか新規作成
done(null, user);
}
));
app.get("/auth/google",
passport.authenticate("google", { scope: ["profile", "email"] })
);clientIDとclientSecretはGoogle Cloud Consoleで発行されます。「こういうアプリを作るので、ユーザー認証をGoogleに任せたい」と登録するプロセスです。
OAuthキーワード整理
Client IDとClient Secret
Google Cloud Consoleでアプリを登録すると2つが発行されます:
| 用語 | 例え | 役割 |
|---|---|---|
| Client ID | 事業者登録番号 | 公開可能。「このアプリが認証を要求しています」の識別 |
| Client Secret | 事業者印鑑 | 絶対非公開。サーバー→Google間の秘密認証にのみ使用 |
Client Secretが漏洩すると、他人が自分のアプリのふりをしてGoogleに認証リクエストを送れてしまいます。
Redirect URI(Callback URL)
Googleログイン後に「認証完了、ここに戻して」と登録するアドレスです:
https://my-lab-app.com/auth/google/callbackGoogle Cloud Consoleに登録したRedirect URIとコードのcallbackURLが完全に一致しなければなりません。1文字でも違えばエラーです。これはセキュリティ対策 — 認証結果が別のサイトに行くのを防止します。
Scope:バレーキーの例え
OAuthのscopeは**「どこまでのアクセスを許可するか」**を定めます。
バレーパーキングを考えてみてください。バレーキー(valet key)を渡すと — エンジンをかけて駐車はできますが、トランクは開けられず、グローブボックスもロックされています。必要な権限だけを限定的に付与するのです。
passport.authenticate("google", { scope: ["profile", "email"] })| scope | アクセス範囲 | バレーキーの例え |
|---|---|---|
profile | 名前、プロフィール写真 | 運転席アクセス(駐車可能) |
email | メールアドレス | ダッシュボード確認(連絡先確認) |
drive.readonly | Google Driveファイル読み取り | トランクを開ける(荷物確認) |
calendar | カレンダー読み書き | 車内のスケジュール帳修正 |
研究室のログインにはprofileとemailで十分です。「この人が誰か」さえわかればいいのですから。不要なscopeを要求するとユーザーが不安になり、Googleがアプリ審査を求めることもあります。
Authorization Code Flow(全体フロー)
OAuth 2.0で最も安全で一般的な方式です:
1. ユーザーが「Googleでログイン」をクリック
→ ブラウザがGoogleログインページに移動
2. ユーザーがGoogleにID/パスワードを入力
→ Googleが「このアプリに名前、メールを提供してもいいですか?」と確認
3. ユーザーが「許可」をクリック
→ GoogleがAuthorization Codeを自サーバーのRedirect URIに送信
4. 自サーバーがAuthorization Code + Client SecretをGoogleに送信
→ GoogleがAccess Tokenを発行
5. 自サーバーがAccess Tokenでユーザープロフィール(名前、メール)を照会
→ Passportのコールバック関数でこの情報でログイン処理核心:Authorization Codeは1回限りの交換券です。これ自体ではユーザー情報を見られず、Client Secretと合わせてAccess Tokenに交換する必要があります。2つが同時に奪取されない限り安全です。
Google OAuthルート設定
実際のExpressルート:
// 1. 「Googleでログイン」ボタンがこのURLに移動
app.get("/auth/google",
passport.authenticate("google", { scope: ["profile", "email"] })
);
// 2. Google認証後ここに戻る(Redirect URI)
app.get("/auth/google/callback",
passport.authenticate("google", { failureRedirect: "/login" }),
function(req, res) {
res.redirect("/dashboard");
}
);ユーザーの視点では「Googleでログイン」ボタン1つを押すだけです。裏側ではAuthorization Code → Access Token → プロフィール照会が自動で行われます。
なぜソーシャルログインを使うのか
| メリット | 説明 |
|---|---|
| ユーザーの利便性 | 新しいパスワードを作る必要がない |
| セキュリティの委任 | パスワードの保存/管理の負担をGoogleに委任 |
| 信頼性 | Googleが二段階認証などを既に提供 |
| 実装が速い | Passportストラテジーのインストール + 設定で完了 |
研究室内部ツールでは特に有用です。研究者はすでにGoogleアカウントを持っているので、別途会員登録なしに「Googleでログイン」一回で即アクセスできます。
全体像:認証システムの階層
これまで学んだことを積み上げます:
[auth-cookies] Cookie & Session — 「このブラウザを記憶する」
↓
[login-system] ログイン — 「誰かを確認する」
↓
├── ローカル認証(メール + パスワード + bcrypt)
├── OAuth 2.0(Google/GitHubソーシャルログイン)
└── 役割ベースのアクセス制御(admin/member/viewer)Cookie/Sessionが「記憶」なら、ログインは「確認」、役割は「権限」です。建物に例えると — 入退室カード(Cookie)があってもすべての部屋に入れるわけではありません。ある部屋はPIカードだけで、ある部屋はすべての研究者カードで開きます。
やってみよう(Faded Example)
空欄を埋めてPassport.jsのローカル認証ストラテジーを完成させてください。
const passport = require("passport");const = require("passport-local").Strategy;const bcrypt = require("bcrypt");passport.use(new LocalStrategy({ usernameField: "" },function(email, password, done) {db.query("SELECT * FROM users WHERE email = ?", [email], function(err, rows) {if (rows.length === 0) return done(null, );const user = rows[0];const isMatch = bcrypt.(password, user.password_hash);if (!isMatch) return done(null, false);return done(null, );});}));
次のステップ
このトピックはログインシステムの構造とフローを理解することが目的です。実際の実装では:
passport-local+bcrypt— メール/パスワードログインの実装passport-google-oauth20— Googleソーシャルログインの追加- 役割ミドルウェア —
requireLogin、requireAdmin等のアクセス制御 - HTTPS — ログイン情報がネットワーク上で平文送信されないように
BioPlayground自体もSupabaseの認証システムを使用しています。直接Passport.jsで実装するにしても、SupabaseのようなBaaSを使うにしても — このトピックで学んだ概念(ハッシュ、OAuthフロー、役割ベース制御)は同じく適用されます。
よくあるエラーと解決法
Q: パスワードをハッシュ化せずに保存するとどうなりますか?
データベースが流出した場合、全ユーザーのパスワードが露出します。多くの人が同じパスワードを複数のサイトで使いまわしているため、一箇所の流出が連鎖的な被害につながります。ハッシュ処理は選択肢ではなく必須です。
Q: OAuthのClient Secretはどこに保存しますか?
コードに直接書いてはいけません。環境変数(.envファイル)に保存し、.gitignoreに.envを追加してGitにアップロードされないようにします。漏洩すると他人が自分のアプリのふりをすることができます。
Q: Passport.jsなしでログインは作れますか?
可能です。bcryptでパスワードを比較し、セッションにユーザー情報を保存すればよいです。しかしソーシャルログイン、セッションのシリアライズ、エラー処理などを自前で実装するとコードが複雑になります。Passport.jsはこの繰り返し作業を減らすフレームワークです。
Q: Supabaseを使えばこれを自分で作る必要はありませんか?
その通りです。Supabaseはメール/パスワード認証、Googleソーシャルログイン、役割ベースのアクセス制御を設定だけで提供します。しかし「Supabaseが内部で何をしているか」を理解するにはこのトピックの概念が必要です。ツールを使っていても原理を知っている人の方が問題解決が速いです。