OAuth 2.0・OpenID Connect入門|認証・認可の仕組みをわかりやすく解説

kento_morota 21分で読めます

「Googleでログイン」「GitHubでログイン」——このようなソーシャルログイン機能は、今や多くのWebサービスで当たり前になっています。この仕組みの裏側で動いているのが、OAuth 2.0とOpenID Connectです。

しかし、これらの仕様は複雑で、正しく理解せずに実装すると深刻なセキュリティリスクにつながります。本記事では、認証と認可の基本概念から、OAuth 2.0とOpenID Connectの仕組み、そして安全な実装方法までをわかりやすく解説します。

認証と認可の違いを正しく理解する

OAuth 2.0とOpenID Connectを学ぶ前に、「認証」と「認可」の違いを明確にしておきましょう。この2つの概念を混同すると、セキュリティ設計で致命的な誤りを犯すことになります。

認証(Authentication)とは

認証は「あなたは誰ですか?」という問いに答えるプロセスです。ユーザーが本人であることを確認します。

日常の例:空港でパスポートを提示して本人確認するのが認証です。パスポートによって「この人は山田太郎さんです」と確認できます。

Webでの例:メールアドレスとパスワードでログインする、生体認証で本人確認する、といったプロセスが認証にあたります。

認可(Authorization)とは

認可は「あなたは何ができますか?」という問いに答えるプロセスです。特定のリソースへのアクセス権限を確認・付与します。

日常の例:ホテルのルームキーがもらえるのが認可です。「このカードで3階の302号室に入れます」と権限が付与されます。ただし、他の部屋には入れません。

Webでの例:「このアプリにGoogleカレンダーの読み取り権限を許可しますか?」という画面で「許可する」をクリックする行為が認可にあたります。

重要なポイント

OAuth 2.0は認可のプロトコルです。「アプリにリソースへのアクセスを許可する」仕組みを提供します。

OpenID ConnectはOAuth 2.0を拡張した認証のプロトコルです。「ユーザーが誰であるかを確認する」仕組みを追加します。

OAuth 2.0だけでは「このアクセストークンを持っている誰か」としかわからず、「その人が誰なのか」は保証されません。認証機能が必要な場合は、OpenID Connectが必要です。

OAuth 2.0の基本的な仕組み

OAuth 2.0は、ユーザーのパスワードを第三者に渡すことなく、リソースへのアクセスを委譲するためのプロトコルです。RFC 6749で標準化されています。

OAuth 2.0の登場人物(ロール)

OAuth 2.0には4つの主要なロールがあります。

リソースオーナー(Resource Owner)
リソース(データ)の所有者。通常はエンドユーザーです。例えば、Googleアカウントを持つユーザーがリソースオーナーです。

クライアント(Client)
リソースへのアクセスを要求するアプリケーション。「Googleカレンダーと連携する予定管理アプリ」がクライアントにあたります。

認可サーバー(Authorization Server)
アクセストークンを発行するサーバー。Googleのログイン・認可画面を提供するサーバーがこれに該当します。

リソースサーバー(Resource Server)
保護されたリソースを提供するサーバー。Google Calendar APIがリソースサーバーにあたります。

認可コードフローの流れ

最も一般的で安全なフローが「認可コードフロー(Authorization Code Flow)」です。サーバーサイドのWebアプリケーションで使用されます。

ステップ1:認可リクエスト
クライアントがユーザーを認可サーバーの認可エンドポイントにリダイレクトします。

// 認可リクエストURLの構築
const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
authUrl.searchParams.set('client_id', 'your-client-id');
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid email profile');
authUrl.searchParams.set('state', generateRandomState()); // CSRF対策

// ユーザーを認可画面にリダイレクト
res.redirect(authUrl.toString());

ステップ2:ユーザーの同意
ユーザーは認可サーバー上でログインし、要求されたアクセス権限を確認して同意します。

ステップ3:認可コードの受け取り
認可サーバーが認可コードをクライアントのコールバックURLに送信します。

// コールバックの処理
app.get('/callback', async (req, res) => {
  const { code, state } = req.query;

  // stateパラメータの検証(CSRF対策)
  if (state !== req.session.oauthState) {
    return res.status(403).send('不正なリクエストです');
  }

  // ステップ4:認可コードをアクセストークンに交換
  const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      code: code,
      client_id: 'your-client-id',
      client_secret: 'your-client-secret',
      redirect_uri: 'https://yourapp.com/callback',
      grant_type: 'authorization_code',
    }),
  });

  const tokens = await tokenResponse.json();
  // tokens = { access_token, refresh_token, expires_in, token_type, id_token }
});

ステップ5:リソースへのアクセス
取得したアクセストークンを使ってAPIにアクセスします。

// アクセストークンでAPIにアクセス
const calendarResponse = await fetch(
  'https://www.googleapis.com/calendar/v3/calendars/primary/events',
  {
    headers: {
      'Authorization': `Bearer ${tokens.access_token}`,
    },
  }
);
const events = await calendarResponse.json();

OAuth 2.0のグラントタイプと使い分け

OAuth 2.0では、アプリケーションの種類に応じて複数のフロー(グラントタイプ)が定義されています。適切なグラントタイプを選択することが、セキュリティを確保する上で非常に重要です。

認可コードフロー+PKCE(推奨)

PKCE(Proof Key for Code Exchange、読み方:ピクシー)は、認可コードの横取り攻撃を防ぐ拡張仕様です。現在では、SPAやモバイルアプリだけでなく、すべてのクライアントタイプでPKCEの使用が推奨されています。

// PKCE用のコードベリファイアとチャレンジを生成
const crypto = require('crypto');

function generateCodeVerifier() {
  return crypto.randomBytes(32).toString('base64url');
}

function generateCodeChallenge(verifier) {
  return crypto.createHash('sha256').update(verifier).digest('base64url');
}

// 認可リクエスト
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);

// セッションに保存
req.session.codeVerifier = codeVerifier;

const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('client_id', clientId);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('redirect_uri', redirectUri);
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('state', generateRandomState());

// トークン交換時にcode_verifierを送信
const tokenResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: authorizationCode,
    redirect_uri: redirectUri,
    client_id: clientId,
    code_verifier: req.session.codeVerifier, // PKCEの検証
  }),
});

クライアントクレデンシャルフロー

ユーザーが関与しない、サーバー間通信(Machine-to-Machine)で使用するフローです。バッチ処理やマイクロサービス間の通信で活用されます。

// サーバー間通信でのトークン取得
const tokenResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: 'service-client-id',
    client_secret: 'service-client-secret',
    scope: 'read:data write:data',
  }),
});

非推奨・廃止されたフロー

インプリシットフロー(Implicit Flow)
以前はSPA向けに推奨されていましたが、アクセストークンがURLフラグメントに含まれるためセキュリティリスクが高く、現在は非推奨です。代わりに認可コードフロー+PKCEを使用してください。

リソースオーナーパスワードクレデンシャルフロー(ROPC)
ユーザーのIDとパスワードをクライアントに直接渡すフローで、OAuth 2.1では正式に廃止されています。

OpenID Connectで認証を実現する

OpenID Connect(OIDC)は、OAuth 2.0の上に構築された認証レイヤーです。OAuth 2.0がアクセス権限の委譲を扱うのに対し、OIDCはユーザーの身元確認(認証)を標準化します。

IDトークンとは

OpenID Connectの核心は「IDトークン」です。IDトークンはJSON Web Token(JWT)形式で、ユーザーの認証情報が含まれています。

// IDトークンのペイロード例(デコード後)
{
  "iss": "https://accounts.google.com",        // 発行者
  "sub": "110169484474386276334",               // ユーザー固有ID
  "aud": "your-client-id",                      // 対象クライアント
  "exp": 1711540800,                            // 有効期限
  "iat": 1711537200,                            // 発行日時
  "nonce": "abc123",                            // リプレイ攻撃防止
  "email": "user@example.com",                  // メールアドレス
  "email_verified": true,                       // メール確認済み
  "name": "山田 太郎",                           // 氏名
  "picture": "https://example.com/photo.jpg"    // プロフィール画像
}

IDトークンの検証手順

IDトークンを受け取ったら、必ず以下の検証を行います。検証を省略すると、偽造されたトークンを受け入れてしまうリスクがあります。

const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

// JWKSエンドポイントから公開鍵を取得
const client = jwksClient({
  jwksUri: 'https://accounts.google.com/.well-known/jwks.json',
  cache: true,
  rateLimit: true,
});

async function verifyIdToken(idToken) {
  // 1. JWTのヘッダーからキーIDを取得
  const decoded = jwt.decode(idToken, { complete: true });
  const kid = decoded.header.kid;

  // 2. 対応する公開鍵を取得
  const key = await client.getSigningKey(kid);
  const publicKey = key.getPublicKey();

  // 3. 署名を検証し、クレームを確認
  const payload = jwt.verify(idToken, publicKey, {
    algorithms: ['RS256'],
    audience: 'your-client-id',    // aud の検証
    issuer: 'https://accounts.google.com', // iss の検証
  });

  // 4. nonceの検証(リプレイ攻撃防止)
  if (payload.nonce !== expectedNonce) {
    throw new Error('nonce が一致しません');
  }

  return payload;
}

UserInfoエンドポイント

IDトークンに含まれない追加のユーザー情報が必要な場合は、UserInfoエンドポイントにアクセスします。

// UserInfoエンドポイントからユーザー情報を取得
const userInfo = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
  headers: {
    'Authorization': `Bearer ${accessToken}`,
  },
});
const user = await userInfo.json();
// { sub, name, given_name, family_name, picture, email, email_verified, locale }

実装時のセキュリティ上の注意点

OAuth 2.0とOpenID Connectを実装する際に、見落としがちなセキュリティ上の注意点をまとめます。

stateパラメータによるCSRF対策

認可リクエストにstateパラメータを含めることで、認可コードの横取りやCSRF攻撃を防止します。

// stateの生成と検証
const crypto = require('crypto');

// 認可リクエスト時
const state = crypto.randomBytes(32).toString('hex');
req.session.oauthState = state;

// コールバック時
if (req.query.state !== req.session.oauthState) {
  throw new Error('state パラメータが一致しません。CSRF攻撃の可能性があります。');
}
delete req.session.oauthState;

トークンの安全な管理

アクセストークン

・有効期限を短く設定する(15分〜1時間程度)
・SPAではメモリ上に保持し、localStorageには保存しない
・サーバーサイドでは暗号化して保存する

リフレッシュトークン

・サーバーサイドで安全に保管する
・リフレッシュトークンローテーションを実装する(使用するたびに新しいトークンを発行)
・不正利用が検出された場合にすべてのトークンを無効化する

// リフレッシュトークンによるアクセストークンの更新
async function refreshAccessToken(refreshToken) {
  const response = await fetch('https://auth.example.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: clientId,
      client_secret: clientSecret,
    }),
  });

  if (!response.ok) {
    // リフレッシュトークンが無効な場合は再認証を要求
    throw new Error('再ログインが必要です');
  }

  const tokens = await response.json();
  // 新しいリフレッシュトークンが返された場合は更新する
  if (tokens.refresh_token) {
    await saveRefreshToken(tokens.refresh_token);
  }
  return tokens.access_token;
}

redirect_uriの厳格な検証

認可サーバー側でredirect_uriを厳格に検証することが極めて重要です。曖昧なマッチングを行うと、オープンリダイレクト攻撃によりトークンが漏洩する危険があります。

// 安全なredirect_uri検証
const allowedRedirectUris = [
  'https://yourapp.com/callback',
  'https://yourapp.com/auth/callback',
];

function validateRedirectUri(uri) {
  // 完全一致で検証する(部分一致やワイルドカードは使わない)
  return allowedRedirectUris.includes(uri);
}

Discovery(ディスカバリ)とメタデータ

OpenID Connectプロバイダーの設定情報は、Discoveryエンドポイントから自動的に取得できます。手動で各エンドポイントURLを設定する代わりに、この仕組みを活用しましょう。

// Discoveryエンドポイントの例
// https://accounts.google.com/.well-known/openid-configuration

// 取得できるメタデータ
{
  "issuer": "https://accounts.google.com",
  "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
  "token_endpoint": "https://oauth2.googleapis.com/token",
  "userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
  "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
  "scopes_supported": ["openid", "email", "profile"],
  "response_types_supported": ["code", "id_token", "code id_token"],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["RS256"]
}
// Discoveryエンドポイントを活用した初期化
async function initializeOIDC(issuer) {
  const discoveryUrl = `${issuer}/.well-known/openid-configuration`;
  const response = await fetch(discoveryUrl);
  const config = await response.json();

  return {
    authorizationEndpoint: config.authorization_endpoint,
    tokenEndpoint: config.token_endpoint,
    userinfoEndpoint: config.userinfo_endpoint,
    jwksUri: config.jwks_uri,
  };
}

まとめ:安全な認証・認可の実装に向けて

OAuth 2.0とOpenID Connectは、現代のWebアプリケーションにおける認証・認可の基盤です。本記事の要点を振り返りましょう。

基本概念の整理
・認証は「誰であるか」の確認、認可は「何ができるか」の確認
・OAuth 2.0は認可のプロトコル、OpenID Connectは認証のプロトコル
・この区別を理解することがセキュアな実装の第一歩

グラントタイプの選択
・WebアプリやSPAには認可コードフロー+PKCEを使用する
・サーバー間通信にはクライアントクレデンシャルフローを使用する
・インプリシットフローやROPCは使用しない

セキュリティ対策の徹底
・stateパラメータとPKCEを必ず使用する
・IDトークンの署名・クレームを必ず検証する
・トークンの有効期限とローテーションを適切に管理する
・redirect_uriは完全一致で検証する

これらの仕組みを正しく理解し、セキュリティのベストプラクティスに従った実装を心がけてください。ライブラリやフレームワークが提供するOAuth/OIDC機能を活用することで、実装の複雑さを軽減しながら安全性を確保できます。

#OAuth#OpenID Connect#認証
共有:
無料メルマガ

週1回、最新の技術記事をお届け

AI・クラウド・開発の最新記事を毎週月曜にメールでお届けします。登録は無料、いつでも解除できます。

プライバシーポリシーに基づき管理します

起業準備に役立つ情報、もっとありますよ。

まずは話だけ聞いてもらう