認証ミドルウェアについて

認証ミドルウェアの実装解説

概要

このドキュメントでは、AWS Cognitoを使用したOAuth 2.0認証フローを実装した認証ミドルウェアについて説明します。 通常の認証フローに加えて、特定の機能専用の認証フロー(CSRF対策付き)の実装方法を解説します。

アーキテクチャ

認証システムは以下の3つの主要コンポーネントで構成されています:

  1. 認証ルート (routes/auth.js) - Cognitoからのコールバックを処理
  2. 認証ミドルウェア (middlewares/auth.js) - リクエストの認証チェック
  3. 認証ライブラリ (lib/Auth.js) - Cognitoとの通信ロジック

通常の認証フロー

フロー概要

ユーザー → ログインページ → Cognito認証 → コールバックエンドポイント → アプリケーション

実装詳細

1. コールバックエンドポイント (/auth/callback)

Cognitoから認可コードを受け取り、アクセストークンとリフレッシュトークンを取得します。

router.get('/auth/callback', (req, res, next) => {
  // 1. 認可コードの検証
  if (!has(req.query, 'code') || !req.query.code) {
    return res.redirect(REDIRECT_FAILURE_URL);
  }

  // 2. 認可コードをアクセストークンに交換
  const tokens = await auth.getTokenFromConsols(req.query.code);
  
  // 3. IDトークンの検証
  const decoded = auth.verifyToken(tokens.id_token);
  
  // 4. トークンをCookieに設定
  res.cookie(HEADER_ACCESSTOKEN_NAME, tokens[HEADER_ACCESSTOKEN_NAME], cookieOptions);
  res.cookie(HEADER_REFRESHTOKEN_NAME, tokens[HEADER_REFRESHTOKEN_NAME], cookieOptions);
  
  // 5. ユーザー情報を取得
  const response = await api.get('/userinfo', config);
  
  // 6. 適切なページへリダイレクト
  res.redirect(successUrl);
});

2. 認証チェックミドルウェア (checkAccessToken)

保護されたルートへのアクセス時に、トークンの有効性を確認します。

module.exports.checkAccessToken = async (req, res, next) => {
  // 1. Cookieにトークンが存在するか確認
  if (!has(req.cookies, HEADER_ACCESSTOKEN_NAME) || 
      !has(req.cookies, HEADER_REFRESHTOKEN_NAME)) {
    return res.redirect('/login');
  }

  try {
    // 2. トークンの検証とユーザー情報の取得
    const { user, accessToken, refreshToken } = await validateTokenAndGetUserInfo(req, res);
    
    // 3. Cookieにユーザー情報を設定
    setUserCookies(res, user, accessToken, refreshToken, req);
    
    next();
  } catch (error) {
    if (error.message === REDIRECT_LOGIN) {
      return res.redirect('/login');
    }
    throw error;
  }
}

3. トークン検証とリフレッシュ

アクセストークンが無効な場合、リフレッシュトークンを使用して新しいトークンを取得します。

const validateTokenAndGetUserInfo = async (req, res) => {
  let accessToken = req.cookies[HEADER_ACCESSTOKEN_NAME];
  let refreshToken = req.cookies[HEADER_REFRESHTOKEN_NAME];
  
  try {
    // アクセストークンでユーザー情報を取得
    response = await getUserInfo(accessToken);
  } catch (error) {
    // 401エラーの場合、リフレッシュトークンで更新
    if (error.response && error.response.status === 401) {
      const refreshResponse = await auth.getRefreshTokenFromConsols(refreshToken);
      
      // 新しいトークンをCookieに設定
      accessToken = refreshResponse.data[HEADER_ACCESSTOKEN_NAME];
      refreshToken = refreshResponse.data[HEADER_REFRESHTOKEN_NAME];
      res.cookie(HEADER_ACCESSTOKEN_NAME, accessToken, cookieOptions);
      res.cookie(HEADER_REFRESHTOKEN_NAME, refreshToken, cookieOptions);
      
      // 再度ユーザー情報を取得
      response = await getUserInfo(accessToken);
    } else {
      throw new Error(REDIRECT_LOGIN);
    }
  }
  
  return { user: response.data.user, accessToken, refreshToken };
};

特定機能専用の認証フロー(CSRF対策付き)

特定の機能(例:外部システム連携ページ)では、認証後に元のページに戻る必要があります。 この場合、CSRF攻撃を防ぐためにstateパラメータを使用した追加のセキュリティ対策を実装します。

フロー概要

ユーザー → 特定機能ページ → 認証チェック → Cognito認証(state付き) → コールバック → 元のページ

実装の特徴

CSRFとは

  1. CSRF対策: ランダムなstate値を生成し、CookieとURLパラメータで検証
  2. コンテキスト保持: 認証前にアクセスしていたページ情報をstateに含めて保持
  3. 有効期限: stateは5分間のみ有効

実装詳細

1. 認証チェックミドルウェア(特定機能専用)

module.exports.checkAccessTokenForSpecificFeature = async (req, res, next) => {
  const resourceId = req.params.resource_id;
  
  // リソースIDの検証(数値のみ許可)
  if (!resourceId || !/^\d+$/.test(resourceId)) {
    return res.redirect('/error/404');
  }
  
  // トークンが存在しない場合、Cognito認証へリダイレクト
  if (!has(req.cookies, HEADER_ACCESSTOKEN_NAME) || 
      !has(req.cookies, HEADER_REFRESHTOKEN_NAME)) {
    
    // Cognito認証URLを構築(stateを含む)
    const { authUrl, state } = auth.buildCognitoAuthUrlForSpecificFeature();
    
    // stateとリソースIDの紐付けをCookieに保存(5分間有効)
    const stateData = {
      state,
      resourceId,
      timestamp: Date.now()
    };
    
    res.cookie('feature_state', JSON.stringify(stateData), {
      ...cookieOptions,
      httpOnly: true,
      maxAge: 300000, // 5分
      sameSite: 'lax'
    });
    
    return res.redirect(authUrl);
  }
  
  // トークンが存在する場合は通常の検証処理
  try {
    const { user, accessToken, refreshToken } = await validateTokenAndGetUserInfo(req, res);
    setUserCookies(res, user, accessToken, refreshToken, req);
    next();
  } catch (error) {
    if (error.message === REDIRECT_LOGIN) {
      return res.redirect('/login');
    }
    throw error;
  }
}

2. Cognito認証URLの構築

buildCognitoAuthUrlForSpecificFeature() {
  const cognitoAuthorizeURL = this.config.cognitoAuthorizeURL;
  
  // ランダムなstate ID生成(CSRF対策)
  const state = crypto.randomBytes(16).toString('hex');
  
  // Cognito認証URL構築
  const redirectUri = `${APP_URL}/auth/callback/feature`;
  const authUrl = `${cognitoAuthorizeURL}?` +
    `response_type=code&` +
    `client_id=${COGNITO_CLIENT_ID}&` +
    `redirect_uri=${encodeURIComponent(redirectUri)}&` +
    `state=${state}`;
  
  return { authUrl, state };
}

3. コールバックエンドポイント(特定機能専用)

router.get('/auth/callback/feature', (req, res, next) => {
  (async () => {
    // 1. codeパラメータの検証
    if (!has(req.query, 'code') || !req.query.code) {
      return res.redirect(REDIRECT_FAILURE_URL);
    }
    
    // 2. stateパラメータの検証
    if (!has(req.query, 'state') || !req.query.state) {
      return res.redirect(REDIRECT_FAILURE_URL);
    }
    
    const receivedState = req.query.state;
    
    // 3. Cookieからstate情報を取得
    if (!has(req.cookies, 'feature_state') || !req.cookies.feature_state) {
      return res.redirect(REDIRECT_FAILURE_URL);
    }
    
    let stateData;
    try {
      stateData = JSON.parse(req.cookies.feature_state);
    } catch (error) {
      return res.redirect(REDIRECT_FAILURE_URL);
    }
    
    // 4. state検証(CSRF対策)
    if (stateData.state !== receivedState) {
      console.error(`State mismatch. Expected: ${stateData.state}, Received: ${receivedState}`);
      return res.redirect(REDIRECT_FAILURE_URL);
    }
    
    // 5. タイムスタンプ検証(5分以内)
    const now = Date.now();
    const elapsed = now - stateData.timestamp;
    if (elapsed < 0 || elapsed > 300000) { // 5分 = 300000ms
      console.error(`State expired. Elapsed: ${elapsed}ms`);
      return res.redirect(REDIRECT_FAILURE_URL);
    }
    
    const resourceId = stateData.resourceId;
    
    // 6. リソースID検証(数値のみ)
    if (!resourceId || !/^\d+$/.test(resourceId)) {
      return res.redirect('/error/404');
    }
    
    // 7. 認可コードをトークンに交換
    const redirectUri = `${process.env.APP_URL}/auth/callback/feature`;
    const tokens = await auth.getTokenFromConsols(req.query.code, redirectUri);
    
    // 8. トークン検証
    const decoded = auth.verifyToken(tokens.id_token);
    
    // 9. トークンをCookieに設定
    res.cookie(HEADER_ACCESSTOKEN_NAME, tokens[HEADER_ACCESSTOKEN_NAME], cookieOptions);
    res.cookie(HEADER_REFRESHTOKEN_NAME, tokens[HEADER_REFRESHTOKEN_NAME], cookieOptions);
    
    // 10. ユーザー情報を取得
    const response = await api.get('/userinfo', config);
    
    // 11. 使用済みのstate Cookieを削除
    res.clearCookie('feature_state', cookieOptions);
    
    // 12. 元のページへリダイレクト
    const redirectUrl = `${process.env.APP_URL}/feature/${resourceId}`;
    res.redirect(redirectUrl);
  })().catch((error) => {
    console.error('Callback error:', error.message);
    res.redirect(REDIRECT_FAILURE_URL);
  });
});

セキュリティ対策

1. CSRF対策

  • stateパラメータ: ランダムな値を生成し、CookieとURLパラメータで二重検証
  • 有効期限: stateは5分間のみ有効
  • タイムスタンプ検証: 経過時間をチェックしてリプレイ攻撃を防止

2. トークン管理

  • HttpOnly Cookie: リフレッシュトークンはHttpOnly Cookieに保存(XSS対策)
  • Secure Cookie: HTTPS接続時のみCookieを送信
  • SameSite属性: CSRF攻撃を軽減

3. 入力検証

  • リソースID検証: 数値のみを許可する正規表現で検証
  • パラメータ存在チェック: 必須パラメータの存在を確認

AWS Cognitoのpromptパラメータ

Cognitoの認証エンドポイントでは、promptパラメータを使用して認証動作を制御できます。

パラメータ値と動作

動作
(なし)セッションがあればログイン画面がスキップされる。セッションがない場合はログイン画面が表示される
noneセッションがあればそのまま処理される。セッションがない場合はlogin_requiredエラーになる
loginセッションの有無に関わらずログイン画面が表示される(再認証)

使用例

サイレント認証(prompt=none

既存のセッションがある場合、ユーザーにログイン画面を表示せずに認証を完了します。

GET /oauth2/authorize?
  response_type=code&
  client_id=YOUR_CLIENT_ID&
  redirect_uri=https://example.com/callback&
  state=YOUR_STATE&
  prompt=none

強制再認証(prompt=login

既存のセッションがあっても、ユーザーに再度ログインを要求します。

GET /oauth2/authorize?
  response_type=code&
  client_id=YOUR_CLIENT_ID&
  redirect_uri=https://example.com/callback&
  state=YOUR_STATE&
  prompt=login

実装での活用

現在の実装ではpromptパラメータは使用していませんが、以下のような用途で活用できます:

  • prompt=none: バックグラウンドでの認証状態確認
  • prompt=login: 重要な操作前の再認証要求

エラーハンドリング

エラーケースと対応

  1. 認可コードなし: ログイン失敗ページへリダイレクト
  2. state不一致: CSRF攻撃の可能性があるため、ログイン失敗ページへリダイレクト
  3. state有効期限切れ: セキュリティ上の理由でログイン失敗ページへリダイレクト
  4. トークン取得失敗: ログイン失敗ページへリダイレクト
  5. トークン検証失敗: ログイン失敗ページへリダイレクト
  6. ユーザー情報取得失敗: ログイン失敗ページへリダイレクト

まとめ

この認証ミドルウェアの実装により、以下の機能を提供しています:

  1. 通常の認証フロー: 標準的なOAuth 2.0認証フロー
  2. 特定機能専用の認証フロー: CSRF対策付きで、認証後に元のページへ戻る機能
  3. トークン自動リフレッシュ: アクセストークンが無効になった場合の自動更新
  4. セキュアな実装: state検証、有効期限チェック、入力検証によるセキュリティ対策
コメント (0)

コメントはまだありません。

コメントを投稿