【Laravel】CognitoとGoogle認証を連携して”ナウい”ログイン機能を実装してみた

こんにちは！

バックエンドエンジニアからフルスタックエンジニアにメガ進化したいすぎちゃんです！ :cat: 

読者の皆さんはポートフォリオを作る中でこんな悩みに直面したことはないでしょうか？


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/abf27c98-5bb3-64c5-a757-6a0c7304c095.png" height=50 width=50/>「**ポートフォリオサイトにログイン機能を実装してみたけど、何かイケてる感じが足りない...**」


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/985f92f7-c3ea-f52e-34a0-a560f8298006.jpeg" height=50 width=50/>「**ユーザーがログインするまでの手間を減らして、もっとスムーズに認証できないかな？**」

せっかくポートフォリオを作るなら、もっとオシャレでスマートなログイン機能を搭載して、インパクトを与えたいですよね。

そこで今回は、AWSの[**Cognito**](https://aws.amazon.com/jp/cognito/)を使いながら、LaravelでGoogle認証を組み合わせて、"カッコいい"認証機能を作る方法をご紹介します！

Googleアカウントを使えば、ユーザー名やパスワードを入力する必要がなくなり、ワンクリックでログイン完了。ユーザーのストレスも減り、操作感もグッと向上すること間違いありません！ :fire: 



## 成果物

![画面収録+2024-09-28+22.07.27_out.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/2170591a-89e4-bd54-3419-b5b5529d218e.gif)


こちらが今回の記事の成果物となります！

実際にGoogle認証は**chatGPT**や**Dropbox**などの大手のサイトで導入されています。

これまで普通のメールアドレス・パスワード形式でログイン画面を作っていた方も、ぜひ今回の内容を参考にして、ポートフォリオの印象を一気にランクアップさせてみてください！

:::note info
- [ChatGPT](https://openai.com/chatgpt/)
- [Dropbox](https://www.dropbox.com/official-teams-page?_tk=paid_sem_goog_biz_b&_camp=21448172102&_kw=dropbox|e&_ad=705059479827||c&gad_source=1&gclid=EAIaIQobChMImeqKsOLkiAMVd4K5BR2TkA1EEAAYASAAEgIn5_D_BwE)
:::

## 事前準備

LaravelでGoogle認証を実装する前に、以下3の項目について事前に設定しておいてください :thumbsup: 

1. Cognitoのユーザープール、ユーザー情報保管用のテーブルを用意
2. Google認証をCognitoと連携
3. `route/web.php`に認証関連のルーティングを追加

### 1. Cognitoのユーザープール、ユーザー情報保管用のテーブルを用意

https://qiita.com/ayasamind/items/9b07c8c2fd433505a0eb

### 2. Google認証をCognitoと連携

https://qiita.com/Naoki1126/items/2dbc8ac4eb07310adc0f

:::note warn
「ホストされたUIを編集」の画面で「**許可されているコールバックURL**」を以下のように設定しておきましょう。

```
http://<自身のアプリケーションのドメイン>/auth/google/callback
```

ローカルホストで開発する場合は`http://localhost:8080/auth/google/callback`とします。
:::

### 3. `route/web.php`に認証関連のルーティングを追加

[**routes/web.php**](https://github.com/sugizakikun/docker-laravel/blob/develop/src/routes/web.php)

```web.php
Route::get('/auth/google', "App\Http\Controllers\Auth\GoogleAuthController@redirectToGoogle")->name('auth.google');
Route::get('/auth/google/callback', "App\Http\Controllers\Auth\GoogleAuthController@handleOAuthCallback")->name('auth.google.callback');
```

- **`/auth/google`**
     Google認証ページにリダイレクトする処理を行います。
     
- **`/auth/google/callback`**
     Google認証後に呼び出されるコールバックURLです。


上記、3つの準備が完了したらLaravelでの実装に進んでいきましょう！

## 実装方法

Google認証によるログインを行なった場合、裏側では以下の流れで処理が行われます。

![スクリーンショット 2024-09-28 22.55.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/970293a6-0bc9-4efd-afba-500259fce4a7.png)

1. 認証の要求を行う
2. 受け取った認証コードを元にアクセストークンを要求する
3. アクセストークンを元にユーザー情報を受け取る
4. ユーザー情報をDBに登録

以下のコードを参考に実装を進めていきましょう :muscle: 

- [GoogleAuthController.php](https://github.com/sugizakikun/docker-laravel/blob/develop/src/app/Http/Controllers/Auth/GoogleAuthController.php)
    GoogleのOAuth認証をトリガーし、Googleからの認証結果を処理するコントローラ。
- [GoogleAuthClient.php](https://github.com/sugizakikun/docker-laravel/blob/develop/src/app/Util/GoogleAuthClient.php)
    Google API との通信を行うクライアントクラス。Googleの認証ページURLの生成、認証コードからトークンを取得する処理、トークンからユーザー情報を取得する処理を担当。
- [HttpClient.php](https://github.com/sugizakikun/docker-laravel/blob/develop/src/app/Util/Common/HttpClient.php)
    Google APIやCognitoなどの外部APIとのやり取りを抽象化するクライアント。
- [CreateGoogleUser.php](https://github.com/sugizakikun/docker-laravel/blob/develop/src/app/Http/Services/Auth/CreateGoogleUser.php)
  Googleユーザーをデータベースに保存するサービスクラスです。既存のユーザーがいれば取得し、なければ新規に作成します。



### 1. 認証の要求を行う

```GoogleAuthController.php
class GoogleAuthController extends Controller
{
     # ... 省略

    public function redirectToGoogle()
    {
        $url = $this->googleAuthClient->getAuthUrl();
        return redirect($url);
    }
```

「**Googleアカウントでログイン**」をタップすると`/auth/google`を経由し、**GoogleAuthControlle**rクラスの`redirectToGoogle`メソッドが呼ばれます。

```GoogleAuthClient.php
class GoogleAuthClient
{
    #... 省略

    public function getAuthUrl()
    {
        return $this->cognitoDomain.
            "/oauth2/authorize".
            "?identity_provider=Google".
            "&redirect_uri=".$this->redirectUrl.
            "&response_type=CODE".
            "&client_id=".$this->appClientId.
            "&scope=email openid";
    }
```

次にGoogle認証の心臓部となる**GoogleAuthClient**クラスのgetAuthUrlが呼び出され、[`/oauth/authorize`](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/authorization-endpoint.html)にGETリクエストを送信します。


なお、GETリクエストのURLは以下の通りです。


```
https://<your-cognito-domain>/oauth2/authorize?identity_provider=Google&redirect_uri=REDIRECT_URL&response_type=CODE&client_id=YOUR_CLIENT_ID&scope=email openid
```

`<your-cognito-domain>`の内容はユーザープール詳細画面の「アプリケーション統合」のタブをクリックすれば確認することができます。

![スクリーンショット 2024-09-28 23.32.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/c388bb76-4f0b-0fe9-bc86-7489570e6fbc.png)

なお、各パラーメータの説明についても念のため記載していきます。


| **パラメータ**          | **説明**                                                                                                                                                           |
|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| identity_provider      |  Cognitoの認証フローにおいて、指定された外部IDプロバイダー（Googleなど）を用いることを指定します。ここでは`Google`と設定します。 |
| redirect_uri | 認証が成功または失敗した後に、ユーザーをリダイレクトする先のURLです。ここでは`http://<自身のアプリケーションのドメイン>/auth/google/callback`を設定します。|
| response_type| コールバックのクエリパラメータに何を返すかを指定します。ここでは`CODE`と指定し、**この認証コードを後でアクセストークンやIDトークンに交換するために使用します。** |
| client_id |CognitoユーザープールのアプリケーションクライアントIDを指定します。**CognitoやGoogleがどのアプリケーションからのリクエストであるかを認識するために使います。** |
| scope | アクセスを要求するスコープ（権限）を指定します。**ユーザーのどの情報にアクセスできるかを定義するパラメータです**。追加で設定をすればGoogleアカウントに紐づいているプロフ画像も参照できます|

### 2. 受け取った認証コードを元にアクセストークンを要求する

1の手順の後、`/auth/google/callback`のURLにクエリパラメータとして`code`が付与されて返ってきます。

```
http://<自身のアプリケーションのドメイン>/auth/google/callback?code=ae56c4fe-f085-4449-b319-b5d21bcfaed9
```

この時に初めて`handleOAuthCallback`メソッドが呼び出されます。

この`code`の値を用いて、トークンエンドポイント(`/oauth/token`)に対してアクセストークンをリクエストします。

https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/token-endpoint.html

```GoogleAuthController.php
class GoogleAuthController extends Controller
{
    private $googleAuthClient;

     #...省略

    public function handleOAuthCallback(CreateGoogleUser $createGoogleUser)
    {
        $code = request()->input('code');

        # 1.コールバックされたトークンを元にアクセストークンを取得
        $getTokenResponse = $this->googleAuthClient->getToken($code);
        
    # ...省略

```

```GoogleAuthClient.php
class GoogleAuthClient
{
    # ... 省略

    public function getToken(string $code)
    {
        $endPoint = $this->cognitoDomain . "/oauth2/token";

        $data = [
            'grant_type'    =>  'authorization_code',
            'client_id'     =>  $this->appClientId,
            'client_secret' =>  $this->appClientSecret,
            'code'          =>  $code,
            'redirect_uri'  =>  $this->redirectUrl
        ];
        $header = 'Content-type: application/x-www-form-urlencoded';

        $response  = $this->httpClient->post($endPoint, $data, $header, false);
        return  json_decode($response, true);
    }

```

```HttpClient.php
class HttpClient
{
    # ... 省略
    public static function post(
        string $url, 
        array $data, 
        string $header = 'Content-type: application/json; charset=UTF-8',
        bool $isJson = true
    ){
        $content = $isJson 
            ? json_encode($data)
            : http_build_query($data);

        // ストリームコンテキストオプションの設定
        $options = [
            'http' => [
                'method'  => 'POST',
                'header'  => $header,
                'content' => $content, // データを送信
                'ignore_errors' => true // エラーを無視してレスポンスを取得する
            ]
        ];

        $context = stream_context_create($options);

        return  file_get_contents($url, false, $context);
    }
```

以上の処理が完了すると、`$getTokenResponse`には以下の情報が代入されます。

![スクリーンショット 2024-09-27 17.23.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/2e07c299-38f8-f647-12bc-3269d9f83acb.png)

| **パラメータ**          | **説明** |
|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| id_token| OpenID Connect（OIDC）フローで使用されるトークン。認証されたユーザーの情報を含むJWT（JSON Web Token）。 |
| access_token |**APIにアクセスする際に用いるトークン**。認可されたリソース（ユーザーのプロフィール、カレンダーなど）にアクセスするために使われる。|
| refresh_token|**有効期限が切れた`access_token`を再取得するためのトークン。**`refresh_token`を使うことで、再度ユーザーの許可を得ることなく新しい`access_token`を取得できる。|
| expires_in|`access_token`の有効期限を示す値です。単位は秒（秒数）です。|
| token_type | 認証ヘッダー（Authorization ヘッダー）でアクセストークンを指定する際に、どのタイプのトークンとして解釈するかを定義します。|




### 3. アクセストークンを元にユーザー情報を受け取る

2で取得した`access_token`をUserInfo エンドポイント(`/oauth/token`)に送信します。

https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/userinfo-endpoint.html


:::note warn
※`Authorization: Bearer <access_token>`をheader情報に設定してください

```
Authorization: Bearer <access_token>
```
:::


```GoogleAuthController.php
class GoogleAuthController extends Controller
{
    # ... 省略

    public function handleOAuthCallback(CreateGoogleUser $createGoogleUser)
    {

        # ... 省略
        $getUserInfoResponse = $this->googleAuthClient->getUserInfo($getTokenResponse);

        # ... 省略
    }
```

```GoogleAuthClient.php
class GoogleAuthClient
{
    # ... 省略

    public function getUserInfo(array $getTokenResponse)
    {
        $endPoint = $this->cognitoDomain . "/oauth2/userInfo";

        $header = 'Authorization: Bearer '. $getTokenResponse['access_token'];

        $response = $this->httpClient->get($endPoint, $header);
        return  json_decode($response, true);
    }

}
```

```HttpClient.php
class HttpClient
{
    public static function get(
        string $url, 
        string $header = 'Content-type: application/json; charset=UTF-8'
    ){
        $options = [
            'http' => [
                'method'=> 'GET',
                'header'=> $header
            ]
        ];

        $context = stream_context_create($options);
        return file_get_contents($url, false, $context);
    }

    # ... 省略
```
これらの処理が完了すると、`$getUserInfoResponse`に以下の情報が代入されます。

![スクリーンショット 2024-09-27 17.20.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/da27f4c0-55b1-3b8c-acab-fd312ff55829.png)

これでやっとCognitoにGoogleユーザーの情報が認知されました！

![スクリーンショット 2024-09-29 23.17.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401561/83a1c5cc-8485-91a5-5311-8fc3fec07da0.png)

↑ご覧の通りcognitoにもGoogleユーザーが追加されていることがわかります :smile_cat: 

### 4. ユーザー情報をDBに登録

次に、Cognitoのユーザープールの情報をアプリケーションのDBに同期するコードを解説します！（初心者も読むことを想定しているので念のため。）

```GoogleAuthController.php
class GoogleAuthController extends Controller
{
    # ... 省略

    public function handleOAuthCallback(CreateGoogleUser $createGoogleUser)
    {
        # ... 省略 
        $user = $createGoogleUser->execute($getUserInfoResponse);
        
        Auth::login($user);

        return redirect(route('home'));
    }
```

```CreateGoogleUser.php
class CreateGoogleUser
{
    public function execute(array $getUserInfoResponse)
    {
        return (new User())->newQuery()
            ->firstOrCreate([
                'email' => $getUserInfoResponse['email'],
                'cognito_username' => $getUserInfoResponse['username']
            ]);
    }
}
```

Cognitoにユーザーがすでに存在するか確認し、いなければ新しく登録（サインアップ）、いれば既存ユーザーとして扱います（サインイン）。


最終的に、取得したユーザー情報をアプリケーションのDBに保存し、アプリケーション内でGoogle認証ユーザーを管理できるようにします :fire:

ユーザーをログイン状態にし、ホーム画面へリダイレクト
`Auth::login($user);` を使ってLaravelの認証機能を用いてユーザーをログイン状態にし、`/home` にリダイレクトすれば完了です!!!!!!


## まとめ

ここまでお読みいただきありがとうございました。

Google認証の実装について、主要なクラスや処理を簡潔に解説しました。

この手順に従って実装を進めて、皆様のポートフォリオのランクアップの一助になれば幸いです! :thumbsup: