Google OAuthの実装方法について

## はじめに
こちらは [エーピーコミュニケーションズ Advent Calendar 2024](https://qiita.com/advent-calendar/2024/ap-com) の 9 日目の記事となります。
普段何気なく利用しているGoogle認証ですが、開発目線としてはどのような作業が必要になるのか実装方法について試してみました。

## 結論
非常に簡単な手順で実装することが可能なため、簡易的な認証などに利用する際には非常に便利に使えちゃいます。

## 作業の流れ
### 前提条件
Googleアカウントを持っていること。

### Google Cloudへのプロジェクト作成
Google Cloudに専用のプロジェクトを作成して、認証機能を設定して行くことになります。

今回はGoogleアカウントについては取得済みであるものとして、プロジェクトの作成から試していきます。

プロジェクト名は`OAuthTest`としました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/8bdd924e-4c02-17ce-3af5-e54789023a34.png)

作成したプロジェクトに切り替えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/242ac737-9079-06a8-1eef-afcf198fee71.png)

### OAuth 同意画面の作成
API と サービスをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/df01b6ef-cfe4-1466-df39-0282c0c70afb.png)

`OAuth 同意画面`を選択し、必要事項の入力へと進みます。
ここではWebアプリなどでの実装を想定しているため、User Typeは`外部`を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/fc201496-0ea9-841e-ded3-842e3a6706f0.png)

アプリ情報にアプリ名を入力します。
ここでは`OAuthTest`としています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/735fc157-b872-f1df-1958-1bf4d2ad1137.png)

ユーザー サポートメール、デベロッパーの連絡先情報のメールアドレスはGoogleアカウントのものにします。
作成者のGmailアドレス以外を選択したい場合は、IAMユーザーをプロジェクトに招待することで選択ができます。

アプリ登録の編集のスコープについては他の権限を利用しない場合にはそのままで良いです。
こちらはGoogleカレンダーやDriveなどにアクセスする必要がある場合は必要なスコープを選択することになるのですが、単純な認証だけであれば不要です。
もしこちらのスコープを利用する場合、本番公開の際に非常に手間になる権限利用のイメージ動画を作成する必要が出てくるので注意です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/4c8945d4-c2bc-256b-753f-1253da3efdf4.png)

## 認証情報の作成
認証情報から `OAuth 2.0 クライアント ID`を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/ea212a35-37b4-f679-c1ca-3e2402aae863.png)

`OAuth クライアント ID`を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/e341aa95-61f0-6e02-e427-144529f1f585.png)

`ウェブアプリケーション`を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/a791957d-d172-2af7-7a43-0b0770bc289c.png)

開発環境で利用する`http://localhost:3000`を入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/28bdf2cf-5b7b-da0f-b1a6-0be8784ad6bb.png)

同様に開発環境で利用する`http://localhost:3000`を入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/062cda01-2bb9-5ee7-1453-a8faaef716a4.png)

作成画面から`クライアント ID`を控えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/50b6c876-80f8-7207-cd0e-6b90908bc9d5.png)

### テスト用認証ページの作成
以下の様なhtmlを作成します。
ここではテストのためファイルに直接取得した`クライアント ID`情報を埋め込んで作成しますが、セキュリティ的な観点からはトークンのベタ書きは推奨されません。
参考にされる際にはアクセス環境や環境に合わせて`承認済みの JavaScript 生成元`と`承認済みのリダイレクト URI`の設定も考慮しつつ修正する必要があります。

```
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Google OAuth Simple Test</title>
    <style>
        body {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            margin: 0;
            font-family: Arial, sans-serif;
        }
        .container {
            text-align: center;
            padding: 20px;
        }
        .user-info {
            margin-top: 20px;
            display: none;
        }
        .user-info img {
            width: 100px;
            height: 100px;
            border-radius: 50%;
            margin-bottom: 10px;
        }
        button {
            padding: 10px 20px;
            margin-top: 20px;
            background-color: #dc3545;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Google OAuth Simple Test</h1>
        <div id="buttonDiv"></div>
        <div id="userInfo" class="user-info">
            <img id="userPicture" alt="User Picture">
            <h2 id="userName"></h2>
            <p id="userEmail"></p>
            <button onclick="handleSignOut()">ログアウト</button>
        </div>
    </div>

    <script src="https://accounts.google.com/gsi/client" async defer></script>
    <script>
        function handleCredentialResponse(response) {
            // JWTをデコード
            const responsePayload = decodeJwtResponse(response.credential);
            
            // ユーザー情報を表示
            document.getElementById('buttonDiv').style.display = 'none';
            document.getElementById('userInfo').style.display = 'block';
            document.getElementById('userPicture').src = responsePayload.picture;
            document.getElementById('userName').textContent = `ようこそ、${responsePayload.name}さん！`;
            document.getElementById('userEmail').textContent = `メールアドレス: ${responsePayload.email}`;
        }

        function decodeJwtResponse(token) {
            const base64Url = token.split('.')[1];
            const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
            const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
                return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
            }).join(''));
            return JSON.parse(jsonPayload);
        }

        function handleSignOut() {
            google.accounts.id.disableAutoSelect();
            document.getElementById('buttonDiv').style.display = 'block';
            document.getElementById('userInfo').style.display = 'none';
        }

        window.onload = function () {
            google.accounts.id.initialize({
                client_id: 'xxxxxxxxxx',
                callback: handleCredentialResponse
            });
            google.accounts.id.renderButton(
                document.getElementById("buttonDiv"),
                { 
                    theme: "outline", 
                    size: "large",
                    type: "standard"
                }
            );
        };
    </script>
</body>
</html> 
```

### ローカル環境での動作確認
こちらアクセスすると簡易的なログイン画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/c608555e-f199-f0fa-a11a-dcb12ef7f6c7.png)

ポップアップでログイン画面の表示、メールアドレスとパスワードを入力。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/4a5c26de-8815-6207-88b3-5b888d630250.png)

承認画面の表示に対しメッセージを確認して次へをクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/a29daefb-8cc3-5086-2b23-07c93f9b9d4e.png)

無事にログインすることができました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/925de8cf-5a86-aea8-4bf6-37670728bf75.png)

ここまでの実装でも認証自体は問題なくできていたのですが、細かい警告やエラーの様なものが出力されていたため、以下の様にGoogleCloud側の設定範囲を広げることにしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/2c886584-3bc3-52b5-6de2-4cac5c1a3572.png)

### 本番環境を想定した動作確認
現在はテスト環境として動作している状態です。
外部URLを認証元として設定する場合、テスト環境の場合はテスト用に登録したユーザーのみが利用可能です。

テストユーザーを追加することで動作確認は可能なのですが、ここでは段階的なテストは不要なため省略し、本番環境として公開をします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/fdc06ab9-e02f-0c3b-e03a-2c9725298d5d.png)

本番環境として、Amazon S3に動作確認用のバケットを用意しました。
静的Webホスティングとして公開設定をし、本番環境で利用するにはhttpsである必要があるため、オブジェクトURLのhttpsで始まるURLを入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/b5154b55-f1ac-4c36-c5e9-da21dff729ac.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/074fda67-8c14-550d-ca6b-202b572de264.png)

### 本番環境公開
アプリの公開をクリックすると確認画面が表示されるため本番環境へと公開します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/fa039f8c-fbf6-164a-8a9f-bf736c3ea5c1.png)

以下の様な条件の際にはGoogleの検証を受ける必要がありますがそのまま確認をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/40ddd879-68b0-edeb-8c67-1e047f688539.png)

公開がされました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/252dd1c0-88b8-0358-1347-ed3709dbd728.png)

実際に本番環境のURLからアクセスしてみたところ、問題なく認証できることが確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939906/925de8cf-5a86-aea8-4bf6-37670728bf75.png)

## おわりに
単純なログイン機能だけの実装であれば思った以上に簡単に利用できます。
認証機能は付けたい場合でも他での認証機能を構築する手間などを省きたい場合には、パパっと利用できるとても良い選択肢となると思います。
ただし利用スコープがある場合には審査が厳しくなるため、覚悟して臨みましょう。