NodejsでreCAPTCHA実装

# 環境

OS: Ubuntu Server 22.04(SSHでローカルコンピュータから接続)
作ったProject名称: MIRAI
Nodejs: Nodejs v20.14.0

# reCAPTCHAの`API key`の取得

[GoogleCloudPlatform](https://www.google.com/recaptcha/admin/create)にアクセスして、`API key`を取得します。

https://www.google.com/recaptcha/admin/create

ラベルという項目に認証を実行するドメインを入力します。
v2かv3かというラジオボタンがありますが、私は厳密に人間のみを通したかったのでv2を選択しました。
プロジェクト名を入力したら送信して完了です。

# フロントエンドの作成

まずは、フロントエンドを作成します。

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>reCAPTCHA</title>
    <script src="https://www.google.com/recaptcha/enterprise.js" async defer></script>
</head>
<body>
    <form action="/submit" method="POST">
        <input type="text" name="name" placeholder="名前">
        <input type="email" name="email" placeholder="メールアドレス">
        <div class="g-recaptcha" data-sitekey="YOUR_SITE_KEY" action="auth"></div>
        <button type="submit">送信</button>
    </form>
</body>
</html>
```

`YOUR_SITE_KEY`の部分に取得した`API key`を入力します。
たったこれだけのコードでreCAPTCHAをページに表示することができます。

# バックエンドの作成

さて、ここからが本題です。
まずは、Nodejsのプロジェクトを作成します。
```bash
$ mkdir MIRAI
$ cd MIRAI
$ npm init -y
$ npm install http @google-cloud/recaptcha-enterprise
```

次に、http moduleを使ってサーバーを立ち上げます。

```javascript
const http = require('http');
const fs = require('fs');
const { RecaptchaEnterpriseServiceClient } = require('@google-cloud/recaptcha-enterprise');

const server = http.createServer((req, res) => {
    if (req.url === '/submit' && req.method === 'POST') {
        let body = '';
        req.on('data', (chunk) => {
            body += chunk.toString();
        });
        req.on('end', () => {
            const token = body.split('=')[1];
            createAssessment(token).then((result) => {
                res.writeHead(200, {'Content-Type': 'text/html'});
                res.end('Success');
            }).catch((err) => {
                res.writeHead(500, {'Content-Type': 'text/html'});
                res.end('Internal Server Error');
            });
        });
    } else {
        res.writeHead(404, {'Content-Type': 'text/html'});
        res.end('404 Not Found');
    }
});
```

ベースのサーバープログラムはいつも通りに書きます。
`createAssessment`関数はreCAPTCHAのAPIを使って認証を行う関数です。
次に、`createAssessment`関数を作成します。これはほとんどGoogleCloudConsoleに載っていたコードをそのまま使います。

```javascript
async function createAssessment(token) {
    const projectID = "YOUR_PROJECT_ID";// ここにプロジェクトIDを入力
    const recaptchaKey = "YOUR_SITE_KEY";// ここにAPI keyを入力
    const recaptchaAction = "auth";
    // reCAPTCHA クライアントを作成する。
    // TODO: クライアント生成コードをキャッシュに保存するか（推奨）、メソッドを終了する前に client.close() を呼び出す。
    const client = new RecaptchaEnterpriseServiceClient();
    const projectPath = client.projectPath(projectID);

    // 評価リクエストを作成する。
    const request = ({
        assessment: {
            event: {
                token: token,
                siteKey: recaptchaKey,
            },
        },
        parent: projectPath,
    });

    const [response] = await client.createAssessment(request);

    // トークンが有効かどうかを確認する。
    if (!response.tokenProperties.valid) {
        console.log(`The CreateAssessment call failed because the token was: ${response.tokenProperties.invalidReason}`);
        return null;
    }

    // 想定どおりのアクションが実行されたかどうかを確認する。
    // The `action` property is set by user client in the grecaptcha.enterprise.execute() method.
    if (response.tokenProperties.action === recaptchaAction) {
        // リスクスコアと理由を取得する。
        // 評価の解釈の詳細については、以下を参照:
        // https://cloud.google.com/recaptcha-enterprise/docs/interpret-assessment
        console.log(`The reCAPTCHA score is: ${response.riskAnalysis.score}`);
        response.riskAnalysis.reasons.forEach((reason) => {
            console.log(reason);
        });

        return response.riskAnalysis.score;
    } else {
        console.log("The action attribute in your reCAPTCHA tag does not match the action you are expecting to score");
        return null;
    }
}
```

`YOUR_PROJECT_ID`と`YOUR_SITE_KEY`の部分にそれぞれプロジェクトIDとAPI keyを入力します。
これで、reCAPTCHAを使って認証を行うことができます。
と思いきや、エラーが出ました。

```bash
Error: Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.
    at GoogleAuth.getApplicationDefaultAsync (/home/ubuntu/MIRAI/node_modules/@google-cloud/recaptcha-enterprise/node_modules/google-auth-library/build/src/auth/googleauth.js:160:19)
    at processTicksAndRejections (internal/process/task_queues.js:93:5)
    at async GoogleAuth.getClient (/home/ubuntu/MIRAI/node_modules/@google-cloud/recaptcha-enterprise/node_modules/google-auth-library/build/src/auth/googleauth.js:496:17)
    at async GrpcClient._getCredentials (/home/ubuntu/MIRAI/node_modules/@google-cloud/recaptcha-enterprise/build/src/v1/recaptcha_enterprise_client.js:107:23)
    at async GrpcClient.createAssessment (/home/ubuntu/MIRAI/node_modules/@google-cloud/recaptcha-enterprise/build/src/v1/recaptcha_enterprise_client.js:116:23)
    at async createAssessment (/home/ubuntu/MIRAI/index.js:23:23)
```

これは、GoogleCloudPlatformにアクセスするための認証情報がないために出るエラーです。
いわゆる`ADC(Application Default Credentials)`がないということです。
これを解決していきます。

# ADC(Application Default Credentials)の取得

## gcloud CLIのインストール

まずは、gcloud CLIをインストールします。
以下のページを参考にしています。

https://zenn.dev/pino0701/articles/gcloud_install

```bash
sudo apt-get update
sudo apt-get install apt-transport-https ca-certificates gnupg curl sudo
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add -
sudo apt-get update && sudo apt-get install google-cloud-cli
```

次にgcloud CLIの初期設定を行います。

```bash
gcloud init
```

以下の情報を求められますので、入力していきます。
1. ログインを行うかの回答
2. 認証用URLを開き、使用するアカウントの選択
3. アクセスリクエストの許可
4. 認証コードのコピー
5. 認証コードの入力
6. デフォルトのプロジェクトの選択

これで、ADC(Application Default Credentials)が取得できました。
わかりにくい点は先ほどの[ページ](https://zenn.dev/pino0701/articles/gcloud_install)に書いてあります。

## Google アカウントのユーザー認証情報を ADC に提供する

以下のコマンドを実行します。

```bash
gcloud auth application-default login
```

先ほどの手順と同じようにログインを行います。
これで、ADC(Application Default Credentials)が取得できました。

# 再度、reCAPTCHAを使って認証を行う

再度、サーバーを立ち上げて、reCAPTCHAを使って認証を行います。

```bash
$ node index.js
```

ブラウザで`http://localhost:3000`にアクセスして、reCAPTCHAを使って認証を行います。
あなたがロボットでなければ、良い感じに認証が通るはずです。

お疲れさまでした。これで無事にreCAPTCHAを使って認証を行うことができました。
GoogleCloudPlatformの説明には、ADC(Application Default Credentials)が必要であることが書いてなかったので、しばらくハマりました。
ADC(Application Default Credentials)が必要であることを覚えておいてください。（従来の方法では`secretKey`を使っていましたが、現在はADC(Application Default Credentials)を使うことで、よりセキュアに認証を行うことができます。）

# 参考記事

- [GoogleCloudPlatform](https://cloud.google.com/recaptcha-enterprise/docs/create-assessment)
- [GoogleCloudDocs](https://cloud.google.com/docs/authentication/provide-credentials-adc)
- [zenn.dev](https://zenn.dev/pino0701/articles/gcloud_install)

## 最後に

この記事のコード引用元になったプロジェクト

https://github.com/JUN-SUZU/MIRAI/

Discordサーバーを運営している方は是非導入を検討してみてください。