一覧へ

研究室専用ログインシステムを作る

Express + Passport.jsで会員登録、ログイン、Googleソーシャルログインを実装する概念とフロー。研究データのアクセス制御の第一歩。

中級
|
60
|
検証済み (2026-06)
ログインPassport.jsOAuth会員登録アクセス制御ソーシャルログイン
進捗0/19 (0%)

研究室専用ログインシステムを作る

これまでHTMLでページを作り、Expressでサーバーを立て、データベースに試料を保存し、Cookieでセッションを管理する方法を学びました。ここまで来ると自然な疑問が生まれます:

「このWebアプリを私たちの研究室メンバーだけが使えるようにできないか?」

試料管理システムに機密性の高い実験データがあるなら、誰でもアクセスできてはいけません。共同研究グループにだけ結果を共有したい場合もあるし、研究発表資料をログインしたレビュアーにだけ見せたい場合もあります。

その時に必要なのがログインシステムです。

Cookieからログインへ

auth-cookiesトピックでCookieとSessionを学びました。「このブラウザはさっきログインした人だ」をサーバーが記憶する仕組みでした。

しかし1つ欠けていたものがあります — 「ログインした人」をどう確認するか? IDとパスワードを受け取り、データベースに保存されているものと比較し、一致すればセッションを作る過程。これを自分で作るとコードが複雑になり、セキュリティのミスが起きやすくなります。

この複雑な認証プロセスを代行してくれるツールがPassport.jsです。

Passport.js:認証専門ミドルウェア

Passport.jsはExpress用の認証ミドルウェアです。「認証はこれを使ってください」と作られたツールです。

bash
npm install passport
npm install passport-local

核心概念は**ストラテジー(Strategy)**です。認証方法ごとに個別のストラテジーパッケージがあります:

ストラテジーパッケージ認証方式
Localpassport-localメール + パスワード
Googlepassport-google-oauth20Googleアカウント
GitHubpassport-github2GitHubアカウント
JWTpassport-jwtJSON Web Token

現在Passport.jsには300以上のストラテジーが登録されています。必要な認証方法のストラテジーだけインストールすれば済みます。

Passport初期設定:Expressに組み込む

PassportをExpressに接続する基本設定です:

javascript
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)**パターンで解決します:

javascript
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)のと同じです。

ローカル認証:メール + パスワード

最も基本的なログイン方式です。ユーザーがメールとパスワードを入力すると、データベースで確認してログインを処理します。

javascript
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保存
  • 以降のリクエストごと:deserializeUserreq.userで現在のログインユーザー情報にアクセス

以前学んだExpressミドルウェア(app.use())と同じパターンです。Passportもミドルウェアとして組み込めば、ルートでreq.userからログインユーザーを確認できます。

会員登録:新しい研究者の追加

ログインがあるにはまず会員登録が必要です。全体のフロー:

text
ユーザー → 登録フォーム(メール、パスワード入力)
→ サーバー:メール重複確認
→ サーバー:パスワードをハッシュ処理(bcrypt)
→ サーバー:ハッシュ化されたパスワードをデータベースに保存
→ ログイン可能に

実際のExpressルートで実装すると:

javascript
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のようなハッシュ関数で変換してから保存します。

javascript
const bcrypt = require("bcrypt");

// 会員登録時:平文 → ハッシュ
const hashedPassword = bcrypt.hashSync("myLabPassword", 10);
// → "$2b$10$X7YKz..." のようなハッシュ値を保存

// ログイン時:入力値のハッシュと保存されたハッシュを比較
const isMatch = bcrypt.compareSync("myLabPassword", hashedPassword);
// → true または false

bcrypt.hashSync(password, 10) — 10はソルトラウンド(ハッシュ反復回数)。数が大きいほど安全ですが遅くなります。10が一般的な推奨値です。

これは研究室のセキュリティと同じです。建物の入退室カードのコードが平文で保存されていれば、データベースが流出した時にすべてのドアが開きます。ハッシュ処理は — カードを複製できなくすることです。

ログイン/ログアウトルート

会員登録後の実際のログイン/ログアウトルート:

javascript
// ログインページの表示
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パッケージが登録するデフォルト名です。

アクセス制御:誰が何を見られるか

ログインシステムの本当の目的はアクセス制御です。「このページはログインした人だけ」「この機能は管理者だけ」— これを実装します。

javascript
// ログインユーザーのみアクセス可能なミドルウェア
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)**が必要です:

javascript
// ユーザーテーブル構造
// 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です。

核心原理を例えで説明すると — ホテルのカードキー発行プロセスと同じです:

text
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 ServerGoogle、GitHubなどフロントデスク(身分確認)

Passport.jsでGoogle OAuthを使うと:

javascript
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"] })
);

clientIDclientSecretは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ログイン後に「認証完了、ここに戻して」と登録するアドレスです:

text
https://my-lab-app.com/auth/google/callback

Google Cloud Consoleに登録したRedirect URIとコードのcallbackURL完全に一致しなければなりません。1文字でも違えばエラーです。これはセキュリティ対策 — 認証結果が別のサイトに行くのを防止します。

Scope:バレーキーの例え

OAuthのscopeは**「どこまでのアクセスを許可するか」**を定めます。

バレーパーキングを考えてみてください。バレーキー(valet key)を渡すと — エンジンをかけて駐車はできますが、トランクは開けられず、グローブボックスもロックされています。必要な権限だけを限定的に付与するのです。

javascript
passport.authenticate("google", { scope: ["profile", "email"] })
scopeアクセス範囲バレーキーの例え
profile名前、プロフィール写真運転席アクセス(駐車可能)
emailメールアドレスダッシュボード確認(連絡先確認)
drive.readonlyGoogle Driveファイル読み取りトランクを開ける(荷物確認)
calendarカレンダー読み書き車内のスケジュール帳修正

研究室のログインにはprofileemailで十分です。「この人が誰か」さえわかればいいのですから。不要なscopeを要求するとユーザーが不安になり、Googleがアプリ審査を求めることもあります。

Authorization Code Flow(全体フロー)

OAuth 2.0で最も安全で一般的な方式です:

text
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ルート:

javascript
// 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でログイン」一回で即アクセスできます。

全体像:認証システムの階層

これまで学んだことを積み上げます:

text
[auth-cookies]    Cookie & Session — 「このブラウザを記憶する」
      ↓
[login-system]    ログイン — 「誰かを確認する」
      ↓
      ├── ローカル認証(メール + パスワード + bcrypt)
      ├── OAuth 2.0(Google/GitHubソーシャルログイン)
      └── 役割ベースのアクセス制御(admin/member/viewer)

Cookie/Sessionが「記憶」なら、ログインは「確認」、役割は「権限」です。建物に例えると — 入退室カード(Cookie)があってもすべての部屋に入れるわけではありません。ある部屋はPIカードだけで、ある部屋はすべての研究者カードで開きます。

やってみよう(Faded Example)

空欄を埋めてPassport.jsのローカル認証ストラテジーを完成させてください。

穴埋め問題javascript
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, );
});
}
));

次のステップ

このトピックはログインシステムの構造とフローを理解することが目的です。実際の実装では:

  1. passport-local + bcrypt — メール/パスワードログインの実装
  2. passport-google-oauth20 — Googleソーシャルログインの追加
  3. 役割ミドルウェアrequireLoginrequireAdmin等のアクセス制御
  4. 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が内部で何をしているか」を理解するにはこのトピックの概念が必要です。ツールを使っていても原理を知っている人の方が問題解決が速いです。

💬 質問・コメント

0件のコメント

ログインせずに投稿できます。ゲスト投稿は投稿者自身で編集・削除できません。

0/2000

読み込み中...