📄
概念 📚 beginner-stepup

認証・認可のパターン

JWT・セッション・OAuthの仕組みを理解し、Webアプリのセキュリティ基盤を設計できるようになる

認証(Authentication)は「あなたが誰か」を確認すること、認可(Authorization)は「あなたに何が許可されているか」を判断することである。この2つは異なる概念だが混同されやすい。Webアプリのほぼすべてがこの仕組みを必要とする。

セッションベース認証

古典的な方式で、サーバー側でログイン状態を管理する:

1. ユーザーがログイン → サーバーがセッションIDを生成
2. サーバーがDB/メモリにセッション情報を保存
3. セッションIDをCookieとしてブラウザに返す
4. 以降のリクエストにCookieが自動付与される
5. サーバーがCookieのセッションIDでユーザーを特定

メリット: セッションを無効化してログアウトを確実に実行できる
デメリット: サーバーがスケールアウトした場合、セッション情報の共有が必要になる

JWTベース認証

現代のSPA・APIで広く使われる方式:

1. ユーザーがログイン → サーバーがJWTトークンを生成して返す
2. クライアントがトークンを保存(localStorage or Cookie)
3. 以降のリクエストのAuthorizationヘッダに付与
   Authorization: Bearer eyJhbGci...
4. サーバーがトークンを検証(署名の確認のみ、DBアクセス不要)
5. トークン内のペイロードからユーザーIDや権限を取得

JWT(JSON Web Token)の構造: ヘッダー.ペイロード.署名 の3部からなるBase64エンコード文字列

ヘッダー:  {"alg": "HS256", "typ": "JWT"}
ペイロード: {"sub": "user_01", "role": "admin", "exp": 1714000000}
署名:     HMACSHA256(base64(header) + "." + base64(payload), secret)

メリット: サーバーがステートレス(セッション保存が不要)、スケールしやすい
デメリット: トークンの即時無効化が難しい(有効期限まで有効)

リフレッシュトークン

JWTの有効期限問題を解決する補助的な仕組み:

  • アクセストークン: 有効期限が短い(15分〜1時間)。APIアクセスに使う
  • リフレッシュトークン: 有効期限が長い(数日〜数週間)。新しいアクセストークンの取得に使う

アクセストークンが切れたらリフレッシュトークンで自動更新し、ユーザーに再ログインを求めない。

OAuth 2.0 / OIDC(ソーシャルログイン)

「Googleでログイン」「GitHubでログイン」の仕組みがOAuth 2.0とその拡張であるOIDC(OpenID Connect):

1. ユーザーが「Googleでログイン」を押す
2. アプリがGoogleの認証ページにリダイレクト
3. ユーザーがGoogleでログインして許可する
4. GoogleがアプリにコードをリダイレクトURLで返す
5. アプリがコードをトークンと交換する(サーバー間通信)
6. GoogleのIDトークンからユーザー情報を取得する

自前で認証基盤を作るより、OAuth/OIDCを使う方がセキュリティリスクを下げられる。

ロールベースアクセス制御(RBAC)

認可の実装パターン。ユーザーに「ロール(役割)」を付与し、ロールによって操作権限を制御する:

type Role = "admin" | "editor" | "viewer"

const permissions = {
  admin:  ["read", "write", "delete"],
  editor: ["read", "write"],
  viewer: ["read"],
}

JWTのペイロードにロールを含めてAPIで参照する。

Dify・n8nで使っているAPIキー認証

DifyのAPIキー(app-xxxx)やn8nのCredentialsは、まさに「APIキー認証」そのものである。

Difyのダッシュボード → 設定 → APIキーを発行 → app-xxxxxxxxxxxx

n8n / Pythonスクリプト → Authorization: Bearer app-xxxxxxxxxxxx → Dify API

このAPIキーが「誰がリクエストしているか」を証明する認証トークンである。自分でWebアプリを作るとき、同じ仕組みを実装することになる。

Dify/n8nのCredentials管理と自作アプリの環境変数管理は本質的に同じ問題である。「APIキーをコードに直書きしない・安全に管理する」という原則は、Dify/n8nの設定でも、自作アプリの .env ファイルでも変わらない(b18参照)。

実装における注意点

  • パスワードは 必ず bcrypt などでハッシュ化 して保存する(平文保存は絶対禁止)
  • HTTPSを使わないとトークンが盗聴される
  • CookieにJWTを保存する場合は HttpOnlySecureSameSite フラグを設定する
  • 短命なアクセストークンとリフレッシュトークンの組み合わせが現代のベストプラクティス

参考リソース


確認クイズ

Q1. 認証(Authentication)と認可(Authorization)の違いを説明してください。

正解: 認証は「あなたが誰か」を確認すること、認可は「あなたに何が許可されているか」を判断すること

解説: ログイン処理は認証、ログイン後にどのページや操作にアクセスできるかを制御するのが認可である。混同しやすいが、設計上は明確に分けて考える必要がある。

Q2. セッションベース認証のデメリットとして正しいものはどれか。 A. トークンの即時無効化が難しい B. サーバースケールアウト時にセッション情報の共有が必要になる C. HTTPSなしで使える D. DBアクセスが不要になる

正解: B. サーバースケールアウト時にセッション情報の共有が必要になる

解説: セッション情報はサーバー側のDB/メモリに保存されるため、複数サーバーに負荷分散すると別サーバーがセッションを認識できない問題が発生する。A(即時無効化が難しい)はJWTのデメリットである。

Q3. JWTの構造として正しいものはどれか。 A. ユーザーID.パスワード.署名 B. ヘッダー.ペイロード.署名 C. セッションID.タイムスタンプ.ロール D. アクセストークン.リフレッシュトークン.有効期限

正解: B. ヘッダー.ペイロード.署名

解説: JWTはヘッダー・ペイロード・署名の3部をドット(.)でつないだBase64エンコード文字列。ペイロードにユーザーIDやロールなどの情報が含まれる。

Q4. JWTベース認証でサーバーがトークンを検証する際、なぜDBアクセスが不要なのか説明してください。

正解: JWTにはサーバーが検証できる署名が含まれており、署名を確認するだけでトークンの正当性を判断できるため

解説: セッションベースではセッションIDをDBで照合する必要があるが、JWTは署名検証のみで完結する。これがJWTをステートレスと呼ぶ理由であり、スケールしやすい根拠でもある。

Q5. リフレッシュトークンを使う目的として正しいものはどれか。 A. パスワードのハッシュ化 B. アクセストークンの有効期限が切れた際に再ログインなしで新しいトークンを取得する C. セッション情報をサーバーに保存する D. HTTPSを不要にする

正解: B. アクセストークンの有効期限が切れた際に再ログインなしで新しいトークンを取得する

解説: アクセストークンは短命(15分〜1時間)に設定してセキュリティリスクを下げ、リフレッシュトークン(数日〜数週間有効)で自動的に更新することでユーザーの利便性を維持する。

Q6. 「Googleでログイン」の仕組みに使われているプロトコルはどれか。 A. SMTP B. OAuth 2.0 / OIDC C. FTP D. WebSocket

正解: B. OAuth 2.0 / OIDC

解説: OAuth 2.0は認可の委譲フレームワーク、OIDCはOAuth 2.0を拡張した認証プロトコル。「Googleでログイン」のようなソーシャルログインはこの仕組みで動いており、自前で認証基盤を作るよりセキュリティリスクを下げられる。

Q7. RBACにおいて「editor」ロールに付与される権限として、このドキュメントの定義に基づき正しいものはどれか。 A. read のみ B. read・write C. read・write・delete D. write・delete

正解: B. read・write

解説: ドキュメントの定義では、adminが「read・write・delete」、editorが「read・write」、viewerが「read」のみ。ロールによって段階的に権限を絞るのがRBACの基本的な考え方である。

Q8. パスワードの保存方法として正しいものはどれか。 A. 平文でDBに保存する B. Base64エンコードして保存する C. bcryptなどでハッシュ化して保存する D. AES暗号化して保存する

正解: C. bcryptなどでハッシュ化して保存する

解説: パスワードは必ずbcryptなどの一方向ハッシュ関数で変換してから保存する。平文やBase64は論外で、AES暗号化は複合可能なため漏洩時のリスクがある。bcryptはブルートフォース攻撃への耐性も持つ。

Q9. CookieにJWTを保存する際に設定すべきフラグを3つ説明してください。

正解: HttpOnly・Secure・SameSite

解説: HttpOnlyはJavaScriptからCookieを読めなくしてXSSを防ぐ。SecureはHTTPS接続時のみCookieを送信するよう制限する。SameSiteはクロスサイトリクエストでのCookie送信を制限してCSRF攻撃を防ぐ。

Q10. DifyのAPIキー(app-xxxx)とn8nのCredentialsは、Webアプリの自作における何に相当するか。

正解: 環境変数(.envファイル)で管理するAPIキー認証トークン

解説: Dify/n8nのAPIキー管理も、自作アプリの.envによる環境変数管理も「APIキーをコードに直書きせず安全に管理する」という同じ問題を解いている。原則に変わりはなく、仕組みの本質を理解することが重要である。

実装編:本カリキュラムでの具体コード

ここから先は Week 3 / Week 14 の実装で使う具体コードです。本カリキュラムでは bcryptjsjose(どちらも純粋 JavaScript 実装で Windows Git Bash でも動く)を採用します。

bcrypt でパスワードをハッシュ化

cd api && pnpm add bcryptjs && pnpm add -D @types/bcryptjs

api/src/lib/password.ts

import bcrypt from "bcryptjs";

// cost=10 は 2026 時点の推奨値。約 60ms 程度
const SALT_ROUNDS = 10;

export async function hashPassword(plain: string): Promise<string> {
  return bcrypt.hash(plain, SALT_ROUNDS);
}

export async function verifyPassword(plain: string, hash: string): Promise<boolean> {
  return bcrypt.compare(plain, hash);
}

登録時と照合時の使い方:

// 登録
const passwordHash = await hashPassword("S3cureP@ss!");
await db.insert(users).values({ email, passwordHash });

// ログイン照合
const [user] = await db.select().from(users).where(eq(users.email, email));
if (!user || !(await verifyPassword(inputPassword, user.passwordHash))) {
  throw new UnauthorizedError("メールアドレスまたはパスワードが違います");
}

重要: verifyPassworduser === undefined の場合でも 同じ時間 かけて処理することで、タイミング攻撃で「メールアドレスが存在するか」がバレるのを防ぎます(上記コードは !user 判定を先にしているため、厳密には改善余地あり。本気でやるならダミーハッシュに対しても compare を走らせる)。

jose でアクセストークン + リフレッシュトークン

cd api && pnpm add jose

api/src/lib/auth.ts

import { SignJWT, jwtVerify, type JWTPayload } from "jose";

const encoder = new TextEncoder();
const ACCESS_SECRET = encoder.encode(process.env.JWT_ACCESS_SECRET ?? "");
const REFRESH_SECRET = encoder.encode(process.env.JWT_REFRESH_SECRET ?? "");

if (ACCESS_SECRET.length === 0 || REFRESH_SECRET.length === 0) {
  throw new Error("JWT_ACCESS_SECRET / JWT_REFRESH_SECRET が .env に未設定");
}

export type TokenClaims = JWTPayload & { userId: string; role?: string };

// 短命:15 分
export async function signAccessToken(userId: string, role?: string) {
  return new SignJWT({ userId, role })
    .setProtectedHeader({ alg: "HS256" })
    .setIssuedAt()
    .setExpirationTime("15m")
    .sign(ACCESS_SECRET);
}

// 長命:30 日
export async function signRefreshToken(userId: string) {
  return new SignJWT({ userId })
    .setProtectedHeader({ alg: "HS256" })
    .setIssuedAt()
    .setExpirationTime("30d")
    .sign(REFRESH_SECRET);
}

export async function verifyAccessToken(token: string): Promise<TokenClaims> {
  const { payload } = await jwtVerify<TokenClaims>(token, ACCESS_SECRET);
  return payload;
}

export async function verifyRefreshToken(token: string): Promise<TokenClaims> {
  const { payload } = await jwtVerify<TokenClaims>(token, REFRESH_SECRET);
  return payload;
}

.env に 2 つの秘密鍵を分けて置く(同じ鍵で両方署名すると、リフレッシュトークンでアクセストークンと同じ検証を通せてしまう事故の元):

JWT_ACCESS_SECRET=長いランダム文字列1(openssl rand -hex 32 で生成)
JWT_REFRESH_SECRET=長いランダム文字列2(別物)

ログイン・リフレッシュ API

// POST /auth/login
authRouter.post("/login", zValidator("json", loginSchema), async (c) => {
  const { email, password } = c.req.valid("json");
  const [user] = await db.select().from(users).where(eq(users.email, email));
  if (!user || !(await verifyPassword(password, user.passwordHash))) {
    throw new UnauthorizedError("メールアドレスまたはパスワードが違います");
  }
  const accessToken = await signAccessToken(user.id, user.role);
  const refreshToken = await signRefreshToken(user.id);
  return c.json({ accessToken, refreshToken, user: { id: user.id, name: user.name } });
});

// POST /auth/refresh
authRouter.post("/refresh", zValidator("json", z.object({ refreshToken: z.string() })), async (c) => {
  const { refreshToken } = c.req.valid("json");
  try {
    const claims = await verifyRefreshToken(refreshToken);
    const [user] = await db.select().from(users).where(eq(users.id, claims.userId as string));
    if (!user) throw new UnauthorizedError();
    const accessToken = await signAccessToken(user.id, user.role);
    return c.json({ accessToken });
  } catch {
    throw new UnauthorizedError("リフレッシュトークンが無効または期限切れ");
  }
});
localStorageHttpOnly Cookie
XSS 耐性❌ JS から読める✅ JS から読めない
CSRF 耐性✅ 自動送信されない❌ SameSite 設定が必要
モバイル✅ Web View でも同じコード△ ドメイン制約で面倒
実装の楽さlocalStorage.setItem だけ△ サーバー側設定も必要

本カリキュラムの方針:

  • アクセストークン: localStorage(短命なので XSS 被害も限定的)
  • リフレッシュトークン: HttpOnly + Secure + SameSite=Strict Cookie(盗まれると被害が大きいので JS から触らせない)

この「ハイブリッド」方式で両方の弱点を緩和します。

フロント側:

// web/src/lib/auth.ts
export async function login(email: string, password: string) {
  const res = await fetch("/api/auth/login", {
    method: "POST",
    credentials: "include",  // Cookie を受け取る/送るため
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email, password }),
  });
  if (!res.ok) throw new Error("login failed");
  const { accessToken, user } = await res.json();
  localStorage.setItem("accessToken", accessToken);
  return user;
}

export function getAccessToken() {
  return localStorage.getItem("accessToken");
}

トークンの即時無効化(Blocklist)

JWT は「有効期限まで無効化できない」のが最大の弱点です。業務で必要になったら:

  • Redis などにトークンの jti(識別子)を Blocklist として登録
  • verifyAccessToken の後に await redis.exists(jti) をチェック
  • パスワード変更・強制ログアウト時に該当トークンを Blocklist に追加

このやり方は「ステートレス」という JWT の利点を部分的に手放します。本カリキュラムでは導入しませんが、概念として覚えておく価値があります。


参考リソース

生きているコード

本ドキュメントで扱ったパターンの完全な動作コードは、メンター側リポジトリの参照ブランチで確認できます。

ブランチの作り方・見方は b00-curriculum-map を参照してください。

📚 beginner-stepup 全 53 章
導入編 13 章
  1. 1. 📄Web とは何か
  2. 2. 📄URL を打ってから画面が表示されるまで
  3. 3. 📄ネットワーク基礎(TCP/IP・DNS・HTTPS)
  4. 4. 📄【発展】物理層から通信が成立するまで(電力・Ethernet・Wi-Fi・Bluetooth)
  5. 5. 📄WSL2・Docker セットアップ詳細(Windows 向け)
  6. 6. 📄環境構築の段階的導入(macOS / Windows)
  7. 7. 📄カリキュラム全体マップ(Week × 教材 × 参照ブランチ × 要求チェックリスト)
  8. 8. 📄このカリキュラムの使い方(SQL・Python・Dify経験者向け)
  9. 9. 📄シェル・ターミナル基礎
  10. 10. 📄Windows で完全にゼロから始める開発環境構築(Week 1)
  11. 11. 📄Git基礎
  12. 12. 📄GitHubワークフロー
  13. 13. 📄パッケージ管理(pnpm workspace)
応用編 16 章
  1. 1. 📄AWSインフラ基礎
  2. 2. 📄AWS Budget Alert の設定(Month 5 Week 17)
  3. 3. 📄環境変数管理
  4. 4. 📄Bastion EC2 と SSH ProxyJump(Month 5 Week 18)
  5. 5. 📄CI/CD基礎
  6. 6. 📄ECR への Docker イメージ push と App EC2 デプロイ(Month 5 Week 19)
  7. 7. 📄テスト設計の基本
  8. 8. 📄CloudFront + S3 + ALB で公開する(Month 5 Week 20)
  9. 9. 📄CLAUDE.md・プロジェクト設定
  10. 10. 📄PR レビュー 5 観点ルーブリック(全 Week 共通)
  11. 11. 📄タスク分解・仕様の書き方
  12. 12. 📄Playwright で E2E テスト(Month 6 Week 22)
  13. 13. 📄生成コードのレビュー・デバッグの勘所
  14. 14. 📄Trivy で脆弱性スキャン(Month 6 Week 23)
  15. 15. 📄CloudWatch Logs の読み方と運用(Month 6 Week 23)
  16. 16. 📄PDF ポートフォリオの自動生成(Month 6 Week 24)