🔒

ALB, Cognito, Lambdaを使ってマルチテナントアプリケーションの開発環境にGoogle認証を導入する

に公開

PeopleXのエンジニアの石村(@kamatama41)です。

PeopleX AI面接では開発環境や社内で利用するアプリケーションに対してIdPとしてGoogle Workspaceを利用したOAuth認証をかけることで外部からのアクセスを制限しています。

通常のALBを使ってGoogleのOAuth認証を設定する場合、以下のようなALBのListener Ruleで type = "authenticate-oidc" を使い直接Googleの認証情報を設定することになるかと思います

Terraformでのコード例
resource "aws_lb_listener_rule" "app" {
  listener_arn = aws_lb_listener.public.arn
  priority     = 100

  condition {
    host_header {
      values = [
        "app.example.com",
      ]
    }
  }

  action {
    type = "authenticate-oidc"
    authenticate_oidc {
      issuer                 = "https://accounts.google.com"
      authorization_endpoint = "https://accounts.google.com/o/oauth2/v2/auth"
      token_endpoint         = "https://oauth2.googleapis.com/token"
      user_info_endpoint     = "https://openidconnect.googleapis.com/v1/userinfo"
      client_id              = var.oidc_client_id
      client_secret          = var.oidc_client_secret
    }
  }

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app.arn
  }
}

認証を適用するアプリケーションの数が固定されている場合はこのような設定で十分なのですが、PeopleX AI面接はマルチテナントアプリケーションでテナントごとにサブドメインが割り振られる仕様[1] のため リダイレクトURI登録問題 が発生します。

具体的にはGoogleのOAuth 2.0クライアントIDに対してアクセスを許可するリダイレクトURIを登録する必要があるのですが

  • ワイルドカードのような仕組みが無いため、テスト用にテナントを増やすとその度にリダイレクトURIを登録しなければならない
  • (2025/08現在) 登録用のAPIなどが存在しないので自動化も難しい

という問題に突き当たりました。
これらの問題を解決するため、Cognito, Lambdaを使った認証の仕組みを構築したのでそれをご紹介します。

※GoogleのリダイレクトURI登録画面。ここに毎回URLを手動で登録する必要がある

仕組みの概要

  • アプリケーション側のListener Ruleは認証Cookieのあるなしで認証を行う
  • 認証Cookieが設定されていない場合、Google認証用のListenerRuleを実行し、認証完了後にLambdaを使ってCookieを設定したうえで元のURLにリダイレクトする

処理の流れ

  • アプリケーションのURL (https://a.app.example.com) へアクセス
  • ALBのアプリケーション用のListener Rule (以下 AppRule) を最初にチェック
  • AppRuleは認証用のCookie (以下 AuthCookie)が設定されていない場合[2], Cognito用のListener Rule(以下 CognitoRule)へ遷移
  • CognitoRuleはUser Poolの情報を確認し、Google認証へ進む
  • Google認証後リダイレクトでCognito経由でCognitoRuleに戻り,actionで設定したLambdaへForwardする
  • Lambda関数でAuthCookieに値を設定し、元のURL (https://a.app.example.com) へリダイレクトする
  • リダイレクト後、再びAppRuleが適用され、AuthCookieを持っているので実際のTargetGroupへルーティングされる
  • このあと別テナント (https://b.app.example.com) へアクセスしてもAuthCookieが設定されているので認証無しでレスポンスが返せる
シーケンス図

ポイント

独自の認証Cookie

ALB Listener Ruleの認証を使うのではなく独自の認証Cookieを利用することで、毎回Google認証をしなくても良くなります。一度Google認証を通れば認証Cookieの有効期間内であればリダイレクトURIに登録していないドメインでも認証を通過でき、「テスト用にテナントを増やすとその度にリダイレクトURIを登録しなければならない」問題を解決できます。

※ただし、1度はGoogle認証を通す必要があるので、例えば自社のテナント (https://peoplex.app.example.com) のようなよく使うテナントは事前にリダイレクトURIに登録する必要があります。

Cookieのローテーション

認証Cookieは固定値を使い続けると不正アクセスの可能性が高まるので、ローテーションを行い定期的に入れ替えています。こちらはTerraformのtime_rotating resourceを利用することで実現しています。

※厳密に言うとtime_rotatingで値が入れ替わるのはrotationの期間が過ぎたあとにterraform applyしたタイミングなのですがPeopleX AI面接の開発環境は頻繁にデプロイ作業を行うためそのタイミングでterraform applyも実施され自然とローテーションされます

Cognito

Googleを直接利用せずにCognito経由でGoogle認証を行うことで、最初にCognitoのリダイレクトURI (https://${domain}.auth.${region}.amazoncognito.com/oauth2/idpresponse) はGoogle側に登録する必要がありますが、そこから先のアプリケーションのドメイン登録はAWS上で完結できます。

これで「リダイレクトURI登録用のAPIなどが存在しないので自動化が難しい」問題を解消しました。

サンプルコード

Cognitoの関連リソース
resource "aws_cognito_user_pool" "main" {
  name = "test-app"
}

# ここのdomainがGoogle側に登録する際に使うドメインになる
# https://${domain}.auth.${region}.amazoncognito.com/oauth2/idpresponse
# のdonainの部分
resource "aws_cognito_user_pool_domain" "main" {
  domain       = "test-app"
  user_pool_id = aws_cognito_user_pool.main.id
}

resource "aws_cognito_identity_provider" "google" {
  user_pool_id  = aws_cognito_user_pool.main.id
  provider_name = "Google"
  provider_type = "Google"

  provider_details = {
    authorize_scopes              = "email"
    client_id                     = var.google_oidc_client_id
    client_secret                 = var.google_oidc_client_secret
    attributes_url                = "https://people.googleapis.com/v1/people/me?personFields="
    authorize_url                 = "https://accounts.google.com/o/oauth2/v2/auth"
    oidc_issuer                   = "https://accounts.google.com"
    token_url                     = "https://www.googleapis.com/oauth2/v4/token"
    attributes_url_add_attributes = "true"
    token_request_method          = "POST"
  }

  attribute_mapping = {
    email    = "email"
    username = "sub"
  }
}

resource "aws_cognito_user_pool_client" "main" {
  name            = "test-app-client"
  user_pool_id    = aws_cognito_user_pool.main.id
  generate_secret = true

  allowed_oauth_flows                  = ["code"]
  allowed_oauth_scopes                 = ["email", "openid"]
  # ここに主要なアプリケーションごとのコールバックURLを指定
  callback_urls                        = [
    "https://peoplex.app.example.com/oauth2/idpresponse",
  ]
  allowed_oauth_flows_user_pool_client = true
  supported_identity_providers         = [aws_cognito_identity_provider.google.provider_name]
}
Lambda関数
exports.handler = async (event) => {
    const cookieName = process.env.COOKIE_NAME;
    const cookieValue = process.env.COOKIE_VALUE;
    const domainName = process.env.DOMAIN_NAME;

    const jwtToken = event.headers['x-amzn-oidc-data'];
    if (!jwtToken) {
        throw Error('Missing x-amzn-oidc-data header');
    }

    // 1日後のGMT形式での日時
    const expirationDate = new Date(Date.now() + 24 * 60 * 60 * 1000).toUTCString();

    const location = `https://${event.headers.host}${event.path}`;

    return {
        statusCode: 302,
        statusDescription: '302 Found',
        isBase64Encoded: false,
        headers: {
            Location: location,
            'Set-Cookie': `${cookieName}=${cookieValue}; Domain=${domainName}; Path=/; Secure; HttpOnly; SameSite=None; Expires=${expirationDate}`
        }
    };
};

Lambda関数デプロイ用のTerraformコード (一部略)

#
# OIDCのCookieの値 (一日ごとに更新)
#
resource "time_rotating" "oidc_credentials" {
  rotation_days = 1
}

resource "random_password" "oidc_cookie_name" {
  length           = 16
  special          = true
  override_special = "_-"

  keepers = {
    rotation = time_rotating.oidc_credentials.id
  }
}

resource "random_password" "oidc_cookie_value" {
  length           = 64
  special          = true
  override_special = "_-"

  keepers = {
    rotation = time_rotating.oidc_credentials.id
  }
}

resource "aws_lambda_function" "oidc_redirect" {
  filename         = data.archive_file.oidc_redirect.output_path
  source_code_hash = filebase64sha256(data.archive_file.oidc_redirect.output_path)
  function_name    = "test-app-oidc-redirect"
  role             = aws_iam_role.lambda_exec.arn
  handler          = "index.handler"
  runtime          = "nodejs22.x"

  environment {
    variables = {
      DOMAIN_NAME  = "example.com"
      COOKIE_NAME  = random_password.oidc_cookie_name.result
      COOKIE_VALUE = random_password.oidc_cookie_value.result
    }
  }
}
ALB Listener Rule (一部省略)
# アプリケーション用のRule
# Cookieをチェックし指定した値だったらアプリケーションにforwardする
resource "aws_lb_listener_rule" "app" {
  listener_arn = aws_lb_listener.public.arn
  priority     = 100

  condition {
    http_header {
      http_header_name = "Cookie"
      values           = ["*${random_password.oidc_cookie_name.result}=${random_password.oidc_cookie_value.result}*"]
    }
  }

  condition {
    host_header {
      values = [
        "*.app.example.com",
      ]
    }
  }

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app.arn
  }
}

# OIDC認証用のリスナールール
# 認証後Cookieを設定する
resource "aws_lb_listener_rule" "oidc" {
  listener_arn = each.value.arn
  priority     = 9999 # 最後に来るように大きな値を設定する

  condition {
    path_pattern {
      values = ["*"]
    }
  }

  action {
    type = "authenticate-cognito"
    authenticate_cognito {
      user_pool_arn              = aws_cognito_user_pool.main.arn
      user_pool_client_id        = aws_cognito_user_pool_client.main.id
      user_pool_domain           = aws_cognito_user_pool_domain.main.domain
      on_unauthenticated_request = "authenticate"
      scope                      = "email openid"
      session_cookie_name        = "test_app_lb_session_cookie"
      session_timeout            = 86400 # 1 day
    }
  }

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.oidc_redirect_lambda.arn
  }
}

脚注
  1. 例えばA社専用ドメインは a.app.example.com, B社は b.app.example.com のようになります ↩︎

  2. もしくは不正な値だった場合 ↩︎

PeopleXテックブログ

Discussion