Cognito で Laravel の認証を実装する

このブログ記事では、Cognitoを使ってLaravelでシンプルな認証機能を実装する方法を紹介します。
2024.05.09

目的

クラスメソッドタイランドの清水です。
本記事では Cognito を使って Laravel で簡単な認証機能を実装します。
認証のシーケンスは以下のようになります。

また、全体の流れを理解するために Laravel のロジックは非常に簡潔になっているので、本番環境で注意が必要な部分は ⚠️ でコメントを書いています。

前提条件・知識

  • AWS アカウントを作成済み
  • IAM Role, Policy, Cloud9 の環境を作成できる権限がある
  • 使いたいAWS アカウントのリージョンで cdk bootstrap コマンドを実行済み
  • ローカル PC に docker, docker-compose をインストール済み

手順

まずは CDK を実行するための環境を Cloud9 で準備します。

Cloud9 の環境を立ち上げる

はじめにCloud9 の環境が使う EC2 インスタンスにアタッチするロールを作成します。
ロールには以下のポリシーを関連付けます。
⚠️ 最小権限ではありません。実際のプロジェクトで使うときは、最小権限を設定したほうが良いです。

  • AWSCloud9SSMInstanceProfile: Session Manager で Cloud9 の環境に接続するためです。
  • 以下の Inline Policy:cdk のデプロイに必要な権限、CloudFormation の Stack を Describe 、Cognito のユーザーを作成する権限です。
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "sts:AssumeRole"
            ],
            "Resource": [
                "arn:aws:iam::*:role/cdk-*"
            ]
        },
        {
            "Sid": "CfnDescribe",
            "Effect": "Allow",
            "Action": [
                "cloudformation:DescribeStacks"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Sid": "CognitoCreateUser",
            "Effect": "Allow",
            "Action": [
                "cognito-idp:AdminCreateUser"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

そして、Cloud9 の環境を作成し、EC2 インスタンスにロールをアタッチします。

ロールのアタッチは以下の記事を参考にしていただければと思います。

Cognito リソースの作成

Cloud9 の環境に接続して、以下のコマンドを実行します。
私が作成した本ブログ用のリポジトリ(https://github.com/yuta-cmth/blog-cognito-laravel)の clone と、必要なパッケージをインストールします。

git clone https://github.com/yuta-cmth/blog-cognito-laravel.git
cd blog-cognito-laravel
npm i

続いて、CDK で使う環境変数の宣言後、cdk コマンドを使ってリソースを作成します。

cdk deploy --require-approval never

コマンド実行後、ターミナルで以下のような出力があれば成功です。

✨ Total time: 33.24s

これで Cognito UserPool が作成できました。
以下のコマンドで UserPool にユーザーを作成します。
<your email address> には自分のメールアドレスを入れます。

export AWS_COGNITO_USER_POOL_ID=$(aws cloudformation describe-stacks --stack-name BlogCognitoLaravelStack --output text --query 'Stacks[0].Outputs[OutputKey==`CognitoUserPoolId`].OutputValue');

aws cognito-idp admin-create-user --user-pool-id $AWS_COGNITO_USER_POOL_ID --temporary-password password --username laravel-test --user-attributes Name=email,Value=<your email address>

ローカル環境で Laravel を立ち上げる

自分のコンピューターで以下のコマンドを実行し、ローカル環境で Laravel と MySQL を立ち上げます。

git clone https://github.com/yuta-cmth/blog-cognito-laravel.git
cd blog-cognito-laravel/codes/blog-cognito-laravel

export AWS_ACCOUNT_ID=<your account id>;
export AWS_REGION=<your region>;
export AWS_COGNITO_USER_POOL_ID=$(aws cloudformation describe-stacks --stack-name BlogCognitoLaravelStack --output text --query 'Stacks[0].Outputs[?OutputKey==`CognitoUserPoolId`].OutputValue');
export AWS_COGNITO_CLIENT_ID=$(aws cloudformation describe-stacks --stack-name BlogCognitoLaravelStack --output text --query 'Stacks[0].Outputs[?OutputKey==`CognitoClientId`].OutputValue');
export AWS_COGNITO_HOSTED_UI_DOMAIN=$(aws cloudformation describe-stacks --stack-name BlogCognitoLaravelStack --output text --query 'Stacks[0].Outputs[?OutputKey==`HostedUIDomain`].OutputValue');
export AWS_COGNITO_CLIENT_SECRET=$(aws cognito-idp describe-user-pool-client --user-pool-id $AWS_COGNITO_USER_POOL_ID --client-id $AWS_COGNITO_CLIENT_ID --output text --query "UserPoolClient.ClientSecret");

./vendor/bin/sail up -d;
./vendor/bin/sail artisan migrate;
./vendor/bin/sail artisan config:clear;
./vendor/bin/sail artisan config:cache;

⚠️ ローカル PC も cloudformation:DescribeStacks 権限が必要になります。

これでCDK で立ち上げたリソースの情報や、AWS アカウント・リージョンを環境変数にセットして、Laravel を起動することができました。

動作確認

http://localhost にアクセスすると、Laravel の Route codes/blog-cognito-laravel/routes/web.php の 8~16 (以下の部分です)が実行されます。

Route::get('/', function (Request $request) {
    $username = $request->session()->get('username');
    $cu = CognitoUser::find($username);
    $user = [];
    if ($username && !empty ($cu?->refresh_token)) {
        $user['username'] = $username;
    }
    return view('home', ['user' => $user]);
})->name('home');

今回はセッションに username というキーが存在し、かつ cognito_users テーブルにセッション username の値に一致するレコードに refresh_token があればログイン済みと見なし、
以下の HTML テンプレート codes/blog-cognito-laravel/resources/views/home.blade.php から HTML を生成してブラウザに返します。
⚠️ このログイン済みかどうかの判定は雑です。本来は refresh token の有効期限は過ぎていないか確認したほうがよいです。

@if (empty($user))
  <div>
    Hello, guest. Please login
  </div>
  <a href="{{ route('login') }}">Login</a>
@else
  <div>
    Hello, {{ $user['username'] }}
  </div>
  <a href="{{ route('logout') }}">Logout</a>
@endif

現在はまだログインしていないので、以下のような画面が表示されます。

ブラウザで「Login」をクリックすると以下の Cognito Hosted UI の画面に遷移するので、以下のように username とパスワードを入れます。
username は laravel-test で、パスワードは password です。

すると、パスワードの再設定が促されるので入力します。

入力して提出すると、Cognito からのコールバックを受け付ける `http://localhost/cognito/login-cb` にリダイレクトし、以下の CognitoController.php ロジックを実行します。
やっていることは、

  1. Cognito のトークンエンドポイントに authorization code を渡して、id token と refresh token を受け取る
  2. ブラウザのセッション username に id token の payload の cognito:username を入れる
  3. cognito_users テーブルに refresh token を入れる
  4. 以上で認証処理を完了とし、トップページにリダイレクトする

になります。
⚠️ 事故を防ぐため、id token は保存しないようにします。
もし id token が必要な場面があれば、refresh token で再発行します。このあたりのロジックはアプリケーションによってまちまちかと思います。
また、本来は id token の検証も必要になります。例えば、id token の payload の aud, iss, token_use, iat です。

    public function loginCb(Request $request)
    {
        $code = $request->query()['code'];
        $token_endpoint = 'https://' . config('aws.cognito.hosted_ui_domain') . '/oauth2/token';
        $client = new \GuzzleHttp\Client();
        $response = $client->request('POST', $token_endpoint, [
            'form_params' => [
                'grant_type' => 'authorization_code',
                'code' => $code,
                'client_id' => config('aws.cognito.client_id'),
                'client_secret' => config('aws.cognito.client_secret'),
                'redirect_uri' => route('cognito.login-cb'),
            ],
        ]);
        $body = $response->getBody();
        $body = json_decode($body, true);
        $id_token = $body['id_token'];

        $payload = explode('.', $id_token)[1];
        $payload = base64_decode($payload);
        $payload = json_decode($payload, true);
        $username = $payload['cognito:username'];
        $refresh_token = $body['refresh_token'];

        session()->put('username', $username);
        CognitoUser::upsert(
            [
                'username' => $username,
                'refresh_token' => $refresh_token,
            ],
            uniqueBy: ['username'],
            update: ['refresh_token'],
        );

        return redirect()->route('home');
    }

これで認証が完了したので、トップページにリダイレクトし、以下の画面が表示されます。
ユーザー名が画面に表示され、ユーザーはログインできたことを確認できます。

リソースの削除

CDK で立ち上げたリソースを以下のコマンドで削除します。

cdk destroy --force

最後に

本記事では、大まかCognito で Laravel の認証機能を実装しました。
今後のブログでは、Cognito での認証を活用する方法を紹介していきたいです。

また、このブログで紹介されているソースコードはすべて https://github.com/yuta-cmth/blog-cognito-laravel で確認できます。