既存のWebアプリで「Amazon Cognito」認証を利用する(その3:PHPのサンプルプログラムの実装:ID Tokenをブラウザに渡さない)

Amazon, AWS

はじめに

以下のエントリの続き。

"Browser" サイドに 「ID Token」 の値をわざわざ返ず、また 「JWT」 の改ざんチェックも行う必要がない方法が実現できました。(そのため、外部ライブラリの利用も不要です。)

環境

  1. ローカルPCでPHPのWebアプリケーションが動作すれば何でも構いません。
    • ( ex : php -S localhost:8080

すでに実装し公開されているした「Github」上のアプリケーションコードは更新しておきました。

認証フローの整理

ほとんど前回の認証フロート変わりません。

変わるのは2点です。

  1. (5)の「Cognito」での認証が終了したタイミングでリダイレクトされるページのURLに code という「クエリ文字列」が付与される
  2. "AppServer" は code クエリ文字列が妥当な値かを「Cognito」に問い合わせる。
    1. 「Congito」の検証が成功すれば、「ID Token」を受け取る。
    2. 「ID Token」を受け取った後はそこからユーザ情報( ex: email )を抜き取り、既存のログイン済み処理に進む

構築

1.「Cognito」に新しい「アプリクライアント」の登録

「Cognito」のユーザープール設定ページから「別のアプリクライアントの追加」をクリックします。

以下のように設定します。

  • アプリクライアント名 : aws-cognito-example ( ※こちらは好きな名前をつけましょう )
  • トークンの有効期限を更新 (日) : 1 ( 既存のWebアプリケーションでログイン済み判定のために一時的に利用したいだけなので短くしています )
  • クライアントシークレットを生成 : チェック

**※前回の id_token を利用する方法では、こちらのチェックは外していましたが今回は必要です。

2.「アプリクライアントの設定」

今度は左のタブから「アプリクライアントの設定」を開きます。

以下のように設定します。

  • 有効な ID プロバイダ : すべて選択
  • コールバック URL : http://localhost:8080/callback.php
  • 許可されている OAuth フロー : Authorization code grant

前回は「Implicit grant」にチェックをしていた点が異なります。

設定できたら保存しましょう。

3. トップページの作成

ログイン用のトップページを作成します。

html/ ディレクトリを作成し、作成してください。

html/index.php

<html>
  <body>
    <div id="container">
      <h1>Cognito OAuth2 Page</h1>

      <h2>Start OAuth2</h2>

      <ul>
<?php
// 設定が必要な環境変数名の一覧
$envVarNames = array(
    'COGNITO_DOMAIN',
    'COGNITO_REGION_ID',
    'COGNITO_USERPOOL_ID',
    'COGNITO_CLIENT_ID',
    'COGNITO_CALLBACK_URL',
    'COGNITO_RESPONSE_TYPE',
);

// 最低限必要な環境変数の設定が行われているかをチェック
foreach ($envVarNames as $name) {
    if (!getenv($name)) {
        echo "<li><strong style=\"color:red\">\"${name}\" is not defined.</strong></li>";
    }
}
?>
      </ul>

      <ul>
        <li>
          <strong>
            <a href="https://<?php echo getenv('COGNITO_DOMAIN'); ?>.auth.<?php echo getenv('COGNITO_REGION_ID'); ?>.amazoncognito.com/login?response_type=<?php echo getenv('COGNITO_RESPONSE_TYPE'); ?>&client_id=<?php echo getenv('COGNITO_CLIENT_ID'); ?>&redirect_uri=<?php echo getenv('COGNITO_CALLBACK_URL'); ?>">LOGIN!</a>
          </strong>
        </li>
      </ul>
    </div>
  </body>
</html>

前回との相違は URLのクエリ文字列 response_type「レスポンスタイプ」 ( COGNITO_RESPONSE_TYPE ) という環境変数から指定できるようにした点です。
前回と同じ挙動をさせるには token という値をセットしてやればよいですが、今回は code という値を設定することを想定しています。

4. コールバックページの作成

html/callback.php

<html>
  <body>
    <div id="container">
      <h1>Cognito Callback Page</h1>
<?php
if (getenv('COGNITO_RESPONSE_TYPE') === 'token') {
    echo '
      <h2>access_type=codeで認証手続きが行われました。</h2>

      <em>自動的に遷移します...</em>

      <script>
        // URLからフラグメント、つまり `#` より後ろの部分を取り出す
        let fragment = document.location.href.replace(/^.*#/mgi, "");
        // フラグメントから "id_token=~" の部分を取り出す
        let idToken = fragment.split("&").find((text) => text.indexOf("id_token") === 0).replace(/^id_token=/mgi, "")

        document.location = `/check_id_token.php?id_token=${idToken}`
      </script>
    ';
} else {
    echo '<h2>access_type=codeで認証手続きが行われました。</h2>';

    // Cognitoに送るPOSTリクエストのボディ情報
    $data = array(
        'code' => $_GET['code'],
        'client_id' => getenv('COGNITO_CLIENT_ID'),
        'scope' => "email profile openid",
        'grant_type' => 'authorization_code',
        'redirect_uri' => getenv('COGNITO_CALLBACK_URL'),
        'client_secret' => getenv('COGNITO_CLIENT_SECRET'),
    );

    // Cognitoに送るPOSTリクエストのURL
    $url = 'https://' . getenv('COGNITO_DOMAIN') . '.auth.' . getenv('COGNITO_REGION_ID') . '.amazoncognito.com/oauth2/token';

    // CognitoにPOSTリクエストを送信し、レスポンスJSONテキスト情報を取得
    $jwtJsonText = file_get_contents(
        $url,
        false,
        stream_context_create(
            array(
                'http' => array(
                    'method' => 'POST',
                    'header' => array(
                        "Content-Type: application/x-www-form-urlencoded",
                    ),
                    'content' => http_build_query($data),
                ),
            )
        )
    );

    if ($jwtJsonText) {
        // ブラウザに"ID Token"を渡さずに、JavaScriptを使わずに認証が完了する
        $jwt = (array) json_decode($jwtJsonText);

        echo '<h3>ID Token</h3>';
        echo '<pre>';
        var_dump($jwt['id_token']);
        echo '</pre>';

        // ID Tokenをカンマ(".")で区切って2番目の要素にemailアドレス情報が付与されている
        echo '<h3>ID Token(base64 decoded)</h3>';
        echo '<pre>';
        var_dump(
            base64_decode(
                explode('.', $jwt['id_token'])[1]
            )
        );
        echo '</pre>';
    }
}
?>
    </div>
  </body>
</html>

環境変数 COGNITO_RESPONSE_TYPE の値が token だった場合には前回同様ですが、 code だった場合は今回追加のロジックに入ります。

受け取ったクエリ文字列 code の値を用いて、サーバサイドからCognitoに問い合わせを行います。
サーバサイドでの問い合わせなのでリダイレクトするわけではないのですが、コールバックURLを引き渡す必要があります。
また、今回は 「クライアントシークレット」 の値が必要です。

動作確認

動作を確認します。

以下のコマンドを実行し、PHPが実行可能なサーバを起動します。

# htmlフォルダに移動
$ cd html/

# php組み込みサーバを起動
$ COGNITO_DOMAIN='aaaaa' \
    COGNITO_REGION_ID='xxxxxxxxxxxxxx' \
    COGNITO_USERPOOL_ID='yyyyyyyyyyyyyyyyyyyyyyyy' \
    COGNITO_CLIENT_ID='zzzzzzzzzzzzzzzzzzzzzzzzzz' \
    COGNITO_CALLBACK_URL='http://localhost:8080/callback.php' \
    COGNITO_RESPONSE_TYPE='code' \
    COGNITO_CLIENT_SECRET=11111111111 \
    php -S localhost:8080

# 以下のように実行しても同様の挙動をします
$ export COGNITO_DOMAIN='aaaaa'
$ export COGNITO_REGION_ID='xxxxxxxxxxxxxx'
$ export COGNITO_USERPOOL_ID='yyyyyyyyyyyyyyyyyyyyyyyy'
$ export COGNITO_CLIENT_ID='zzzzzzzzzzzzzzzzzzzzzzzzzz'
$ export COGNITO_CALLBACK_URL='http://localhost:8080/callback.php'
$ export COGNITO_RESPONSE_TYPE='code'
$ export COGNITO_CLIENT_SECRET=11111111111
$ php -S localhost:8080

いずれの環境変数も「Cognito」のユーザープール設定ページで取得できるので適切な値を設定しましょう。

うまく行けば以下のようなページが表示されます。

LOGIN! リンクをクリックすれば「Cognito」の認証フローに進みます。


「ID Token」の取得が終了すれば、取得できたメールアドレスに該当するユーザはログイン済み扱いとして、既存のWebアプリケーションのログイン済み処理へと進めばよいわけです。

ひとこと

前回は index.php / callback.php / check_id_token.php の3ページを作成しましたが、この方式だと2ページで実現できました。

Amazon, AWS