ウェブ用の Google ワンタップ ログイン プロンプト

1. はじめに

この Codelab は、ウェブ用の Google でログインボタンの Codelab を基にしています。そのため、まずそちらを完了してください。

この Codelab では、Google Identity Services JavaScript ライブラリとワンタップ プロンプトを使用して、HTML と JavaScript API を使用して静的ウェブページと動的ウェブページにユーザー ログインを追加します。

One Tap プロンプト

また、JWT ID トークンを検証するためのサーバーサイドのログイン エンドポイントも設定します。

学習内容

  • サーバーサイドのログイン エンドポイントを設定して ID トークンを検証する方法
  • ウェブページに Google One Tap プロンプトを追加する方法
    • 静的 HTML 要素として、
    • JavaScript を使用して動的に追加します。
  • One Tap プロンプトの動作

必要なもの

  1. HTML、CSS、JavaScript、Chrome DevTools(または同等のツール)に関する基本的な知識。
  2. HTML ファイルと JavaScript ファイルを編集してホストする場所。
  3. 前の Codelab で取得したクライアント ID
  4. 基本的な Python アプリを実行できる環境。

始めましょう!

2. ログイン エンドポイントを設定する

まず、基本的なウェブサーバーとして機能する Python スクリプトを作成し、それを実行するために必要な Python 環境を設定します。

ローカルで実行すると、スクリプトはランディング ページと静的 HTML、動的ワンタップ ページをブラウザに提供します。POST リクエストを受け入れ、認証情報パラメータに含まれる JWT をデコードし、Google Identity の OAuth プラットフォームによって発行されたことを検証します。

JWT をデコードして検証すると、スクリプトは index.html ランディング ページにリダイレクトされ、結果が表示されます。

このコードを simple-server.py という名前のファイルにコピーします。

"""Very basic web server to handle GET and POST requests."""
from http.server import SimpleHTTPRequestHandler
import json
import socketserver
from typing import Dict, Optional, Tuple
import urllib.parse
from urllib.parse import parse_qs

from google.auth.transport import requests as google_auth_requests
from google.oauth2 import id_token


""" NOTE: You'll need to change this """
CLIENT_ID = (
    "PUT_YOUR_WEB_CLIENT_ID_HERE"
)

""" these may change for a Cloud IDE, but good as-is for local termainals """
SERVER_ADDRESS = "0.0.0.0"
PORT = 3000
TARGET_HTML_PAGE_URL = f"http://localhost:{PORT}/"
""" and this is the end of constants you might need to change """

HTTP_STATUS_OK = 200
HTTP_STATUS_BAD_REQUEST = 400
HTTP_STATUS_UNAUTHORIZED = 401
HTTP_STATUS_INTERNAL_SERVER_ERROR = 500
HTTP_STATUS_FOUND = 303  # For redirection after decode and verify
OIDC_SERVER = "accounts.google.com"


class OIDCJWTReceiver(SimpleHTTPRequestHandler):
  """Request handler to securely process a Google ID token response."""

  def _validate_csrf(self, request_parameters: Dict) -> Tuple[bool, str]:
    """Validates the g_csrf_token to protect against CSRF attacks."""
    csrf_token_body = request_parameters.get("g_csrf_token")
    if not csrf_token_body:
      return False, "g_csrf_token not found in POST body."

    csrf_token_cookie = None
    cookie_header = self.headers.get("Cookie")
    if cookie_header:
      cookie_pairs = (c.split("=", 1) for c in cookie_header.split(";"))
      cookies = {k.strip(): v.strip() for k, v in cookie_pairs}
      csrf_token_cookie = cookies.get("g_csrf_token")
    if not csrf_token_cookie:
      return False, "g_csrf_token not found in cookie."

    if csrf_token_body != csrf_token_cookie:
      return False, "CSRF token mismatch."

    return True, "CSRF token validated successfully."

  def _parse_and_validate_credential(
      self, request_parameters: Dict
  ) -> Optional[Tuple[Optional[Dict], str]]:
    """Parse POST data, extract, decode and validate user credential."""
    credential = request_parameters.get("credential")
    if not credential:
      return None, "Credential not provided"

    try:
      id_info = id_token.verify_oauth2_token(
          credential, google_auth_requests.Request(), CLIENT_ID
      )
      return id_info, ""
    except ValueError as e:
      return None, f"Error during JWT decode: {e}"
    except Exception as e:
      return None, f"Unexpected error during credential validation: {e}"

  def _redirect_to_html(self, response_data: Dict) -> None:
    """Redirect to the target HTML page with data in the URL fragment."""
    try:
      json_data = json.dumps(response_data)
      encoded_data = urllib.parse.quote(json_data)
      redirect_url = f"http://localhost:{PORT}/#data={encoded_data}"
      self.send_response(HTTP_STATUS_FOUND)
      self.send_header("Location", redirect_url)
      self.send_header("Connection", "close")
      self.end_headers()
    except Exception as e:
      print(f"An error occurred during redirection: {e}")
      self.send_response(HTTP_STATUS_INTERNAL_SERVER_ERROR)
      self.send_header("Content-type", "text/plain")
      self.send_header("Connection", "close")
      self.end_headers()
      self.wfile.write(f"A redirect error occurred: {e}".encode("utf-8"))

  def _send_bad_request(self, message: str) -> None:
    """Sends a 400 Bad Request response."""
    self.send_response(HTTP_STATUS_BAD_REQUEST)
    self.send_header("Content-type", "text/plain")
    self.send_header("Connection", "close")
    self.end_headers()
    self.wfile.write(message.encode("utf-8"))

  def do_POST(self):
    """Handle POST requests for the /user-login path."""
    if self.path != "/user-login":
      self.send_error(404, "File not found")
      return

    try:
      content_length = int(self.headers.get("Content-Length", 0))
      post_data_bytes = self.rfile.read(content_length)
      post_data_str = post_data_bytes.decode("utf-8")
      request_parameters = {
          key: val[0]
          for key, val in parse_qs(post_data_str).items()
          if len(val) == 1
      }

      csrf_valid, csrf_message = self._validate_csrf(request_parameters)
      if not csrf_valid:
        print(f"CSRF verify failure: {csrf_message}")
        self._send_bad_request(f"CSRF verify failure: {csrf_message}")
        return

      decoded_id_token, error_message = self._parse_and_validate_credential(
          request_parameters
      )

      response_data = {}
      if decoded_id_token:
        response_data["status"] = "success"
        response_data["message"] = decoded_id_token
      elif error_message:
        response_data["status"] = "error"
        response_data["message"] = error_message
      else:
        response_data["status"] = "error"
        response_data["message"] = "Unknown error during JWT validation"

      self._redirect_to_html(response_data)

    except Exception as e:
      self._redirect_to_html(
          {"status": "error", "error_message": f"Internal server error: {e}"}
      )


with socketserver.TCPServer(("", PORT), OIDCJWTReceiver) as httpd:
  print(
      f"Serving HTTP on {SERVER_ADDRESS} port"
      f" {PORT} (http://{SERVER_ADDRESS}:{PORT}/)"
  )
  httpd.serve_forever()

ID トークンのオーディエンス(aud)フィールドを使用して、JWT がクライアントに発行されたことを確認するため、Python アプリはどのクライアント ID が使用されているかを知る必要があります。これを行うには、PUT_YOUR_WEB_CLIENT_ID_HERE を、前の「Google でログイン」ボタンの Codelab で使用したクライアント ID に置き換えます。

Python 環境

ウェブサーバー スクリプトを実行する環境をセットアップしましょう。

JWT の検証とデコードを行うには、Python 3.8 以降といくつかのパッケージが必要です。

$ python3 --version

python3 のバージョンが 3.8 より前の場合は、目的のバージョンが見つかるようにシェル PATH を変更するか、システムに新しいバージョンの Python をインストールする必要があります。

次に、JWT のデコードと検証に必要なパッケージを一覧表示する requirements.txt という名前のファイルを作成します。

google-auth

simple-server.py および requirements.txt と同じディレクトリで次のコマンドを実行して、仮想環境を作成し、このアプリ専用のパッケージをインストールします。

$ python3 -m venv env
$ source env/bin/activate
(env) $ pip install -r requirements.txt

サーバーを起動します。すべてが正常に動作していれば、次のように表示されます。

(env) $ python3 ./simple-server.py
Serving HTTP on 0.0.0.0 port 3000 (http://0.0.0.0:3000/) ...

クラウドベースの IDE

この Codelab はローカル ターミナルと localhost で実行するように設計されていますが、Replit や Glitch などのプラットフォームでも、変更を加えれば使用できる可能性があります。プラットフォームごとに設定要件と Python 環境のデフォルトが異なるため、TARGET_HTML_PAGE_URL や Python の設定など、いくつかの変更が必要になる可能性があります。

たとえば、Glitch では requirements.txt を追加するだけでなく、start.sh という名前のファイルを作成して Python サーバーを自動的に起動します。

python3 ./simple-server.py

Python スクリプトと HTML ファイルで使用される URL も、Cloud IDE の外部 URL に更新する必要があります。したがって、TARGET_HTML_PAGE_URL = f"https://your-project-name.glitch.me/" のようになります。この Codelab の HTML ファイルもデフォルトで localhost を使用するため、外部 Cloud IDE URL(data-login_uri="https://your-project-name.glitch.me/user-login")で更新する必要があります。

3. ランディング ページを作成する

次に、One Tap でのログインの結果を表示するランディング ページを作成します。このページには、デコードされた JWT ID トークンまたはエラーが表示されます。ページのフォームを使用して、JWT を Python HTTP サーバーのログイン エンドポイントに送信することもできます。このエンドポイントで JWT がデコードされ、検証されます。CSRF ダブル送信 Cookie と POST リクエスト パラメータを使用して、コードラボの gsi/client HTML と JavaScript API の例と同じ user-login サーバー エンドポイントを再利用できるようにします。

ターミナルで、これを index.html という名前のファイルに保存します。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>JWT Verification</title>
    <style>
      body { font-family: sans-serif; margin: 0; }
      .top-nav {
        text-align: center; padding: 15px 0; background-color: #f8f9fa;
        border-bottom: 1px solid #dee2e6; width: 100%;
      }
      .top-nav a {
        margin: 0 15px; text-decoration: none; font-weight: bold;
        color: #007bff; font-size: 1.1em;
      }
      .top-nav a:hover { text-decoration: underline; }
      .page-container { padding: 20px; }
      pre {
        background-color: #f9f9f9; padding: 10px; overflow-x: auto;
        white-space: pre-wrap; word-break: break-all;
      }
      .error { color: red; }
      .success { color: green; }
      #jwt-form { margin-bottom: 20px; flex: 1; }
      fieldset {
        border: 1px solid #ddd; padding: 10px; margin: 0;
        min-width: 0; flex: 1; }
      legend { font-weight: bold; margin-bottom: 5px; }
      textarea { width: 100%; box-sizing: border-box; vertical-align: top; }
      button[type="submit"] { padding: 8px 15px; margin-top: 10px; }
      @media (min-width: 1024px) {
        .main-content { display: flex; gap: 20px; }
        .main-content > #jwt-form,
        .main-content > .result-container {
          flex-basis: 50%; /* Each item takes up 50% of the width */
          margin-bottom: 0; /* Remove bottom margin when side-by-side */
          display: flex; /* Make the result container a flex container */
          flex-direction: column; /* Stack children vertically */
          flex: 1; /* Allows the result container to grow and shrink */
        }
      }
    </style>
  </head>
  <body>
    <nav class="top-nav">
      <a href="static-page.html">One Tap Static Page</a>
      <a href="dynamic-page.html">One Tap Dynamic Page</a>
      <a href="prompt-outcomes.html">Prompt behaviors</a>
    </nav>
    <div class="page-container">
      <h1>JWT Verification</h1>

      <div class="main-content">
        <form id="jwt-form" action="/user-login" method="post">
          <fieldset>
            <legend>Encoded JWT ID Token</legend>
            <textarea id="credential" name="credential" rows="5"
                      cols="50"></textarea>
            <button type="submit">Verify JWT</button>
          </fieldset>
        </form>

        <section class="result-container">
          <fieldset>
            <legend>Decode and verify result</legend>
            <p id="status"></p>
            <pre id="result"></pre>
          </fieldset>
        </section>
      </div>
    </div>
    <script>
      const statusElement = document.getElementById("status");
      const resultElement = document.getElementById("result");

      const handleResponse = (responseData) => {
        const { status, message } = responseData;
        const result = message
          ? status === "success"
            ? JSON.stringify(message, null, 2)
            : message
          : "";

        statusElement.textContent = status;
        resultElement.textContent = result;
        statusElement.className = "";
        if (status === "success") {
          statusElement.classList.add("success");
        } else if (status === "error") {
          statusElement.classList.add("error");
        }
      };

      const getEncodedDataFromHash = (hash) => {
        const urlParams = new URLSearchParams(hash.substring(1));
        return urlParams.get("data");
      };

      const processHashData = (hash) => {
        const encodedData = getEncodedDataFromHash(hash);
        if (encodedData) {
          try {
            const jsonData = JSON.parse(decodeURIComponent(encodedData));
            handleResponse(jsonData);
            history.pushState(
              "",
              document.title,
              window.location.pathname + window.location.search,
            );
          } catch (error) {
            handleResponse({
              status: "error",
              message: "Error parsing data from URL: " + error,
            });
          }
        }
      };

      window.addEventListener("load",
                              () => processHashData(window.location.hash));
      window.addEventListener("hashchange",
                              () => processHashData(window.location.hash));
    </script>

    <script>
      document.addEventListener("DOMContentLoaded", () => {

        const generateRandomString = (length) => {
        const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
                      "abcdefghijklmnopqrstuvwxyz" +
                      "0123456789";
          let result = "";
          for (let i = 0; i < length; i++) {
            result += chars.charAt(Math.floor(Math.random() * chars.length));
          }
          return result;
        };

        const csrfToken = generateRandomString(12);
        document.cookie = `g_csrf_token=${csrfToken};path=/;SameSite=Lax`;
        const form = document.getElementById("jwt-form");
        const hiddenInput = document.createElement("input");
        hiddenInput.setAttribute("type", "hidden");
        hiddenInput.setAttribute("name", "g_csrf_token");
        hiddenInput.setAttribute("value", csrfToken);
        form.appendChild(hiddenInput);
      });
    </script>
  </body>
</html>

ウェブサーバーと JWT デコードをテストする

ワンタップを試す前に、サーバー エンドポイント環境が設定され、動作していることを確認します。

ランディング ページ http://localhost:3000/ を参照し、[Verify JWT] ボタンを押します。

次のように表示されます。

コンテンツのない JWT 検証

ボタンを押すと、entryfield の内容を含む POST が Python スクリプトに送信されます。スクリプトは、エンコードされた JWT が entryfield に存在することを想定しているため、ペイロードのデコードと検証を試みます。その後、ランディング ページにリダイレクトされ、結果が表示されます。

しかし、JWT がありませんでした。これは失敗したのではないでしょうか?はい、ただし、優雅に!

フィールドが空であるため、エラーが表示されます。エントリ フィールドにテキスト(任意のテキスト)を入力し、ボタンをもう一度押してみましょう。別のデコードエラーで失敗します。

エンコードされた Google 発行の JWT ID トークンを入力フィールドに貼り付けて、Python スクリプトでデコード、検証、表示することもできます。また、https://jwt.io を使用して、エンコードされた JWT を検査することもできます。

4. 静的 HTML ページ

次に、JavaScript を使用せずに HTML ページで動作するようにワンタップを設定します。これは、静的サイトやキャッシュ保存システム、CDN に役立ちます。

まず、次のコードサンプルを static-page.html という名前のファイルに追加します。

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://accounts.google.com/gsi/client" async></script>
    <link rel="icon" href="data:," />
  </head>
  <body>
    <h1>Google One Tap static HTML page</h1>
    <div
      id="g_id_onload"
      data-client_id="PUT_YOUR_WEB_CLIENT_ID_HERE"
      data-ux_mode="redirect"
      data-login_uri="http://localhost:3000/user-login"
    ></div>
  </body>
</html>

次に、static-page.html を開き、PUT_YOUR_WEB_CLIENT_ID_HERE を前の「Google でログイン」ボタンの Codelab で使用したクライアント ID に置き換えます。

このコードで何を行っているのか見ていきましょう。

idg_id_onload の HTML 要素とそのデータ属性は、Google Identity Services ライブラリ(gsi/client)の構成に使用されます。また、ドキュメントがブラウザに読み込まれると、ワンタップ プロンプトが表示されます。

data-login_uri 属性は、ユーザーがログインした後にブラウザから POST リクエストを受信する URI です。このリクエストには、Google が発行したエンコード済みの JWT が含まれています。

ワンタップ オプションの一覧については、HTML コード ジェネレータHTML API リファレンスをご覧ください。

ログイン

http://localhost:3000/static-page.html をクリックします。

ブラウザにワンタップ プロンプトが表示されます。

One Tap UI

[Continue as] を押してログインします。

ログイン後、Google は Python サーバーのログイン エンドポイントに POST リクエストを送信します。リクエストには、Google によって署名されたエンコード済みの JWT が含まれています。

サーバーは、Google の公開署名鍵のいずれかを使用して、Google が JWT を作成して署名したことを確認します。次に、オーディエンスをデコードして、クライアント ID と一致するかどうかを確認します。次に、CSRF チェックが行われ、Cookie の値と POST 本文のリクエスト パラメータの値が等しいことが確認されます。そうでない場合は、問題が発生していることを示す確実な兆候です。

最後に、ランディング ページに、検証済みの JWT が JSON 形式の ID トークン ユーザー認証情報として表示されます。

検証済みの JWT

一般的なエラー

ログインフローが失敗する原因はいくつかあります。一般的な理由は次のとおりです。

  • data-client_id がないか、正しくない場合、DevTools コンソールにエラーが表示され、ワンタップ プロンプトが機能しません。
  • URI が正しく入力されていない、ウェブサーバーが起動していない、または間違ったポートでリッスンしているため、data-login_uri を使用できません。この場合、ワンタップ プロンプトは機能しているように見えますが、認証情報が返されると、DevTools の [ネットワーク] タブにエラーが表示されます。
  • ウェブサーバーが使用しているホスト名またはポートが、OAuth クライアント ID承認済みの JavaScript 生成元にリストされていません。コンソールに「ID アサーション エンドポイントの取得時に、400 HTTP レスポンス コードが受信されました。」というメッセージが表示されます。このコードラボでこのエラーが表示された場合は、http://localhost/http://localhost:3000 の両方がリストされていることを確認してください。

5. 動的ページ

次に、JavaScript 呼び出しを使用してワンタップを表示します。この例では、ページが読み込まれるたびに常にワンタップが表示されますが、必要に応じてプロンプトを表示することもできます。たとえば、ユーザーのセッションが 28 日以上経過しているかどうかを確認し、ログイン プロンプトを再度表示することが考えられます。

次のコードサンプルを dynamic-page.html という名前のファイルに追加します。

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://accounts.google.com/gsi/client" async></script>
    <link rel="icon" href="data:," />
  </head>
  <body>
    <h1>Google One Tap dynamic page</h1>
    <script>
      const generateRandomString = (length) => {
        const array = new Uint8Array(length / 2);
        window.crypto.getRandomValues(array);
        return Array.from(array, (byte) =>
          byte.toString(16).padStart(2, "0")
        ).join("");
      };

      const setCookie = (name, value) => {
        document.cookie = '${name}=${value};path=/;SameSite=Lax';
      };

      const getCookie = (name) => {
        const nameEQ = name + "=";
        const ca = document.cookie.split(";");
        for (let i = 0; i < ca.length; i++) {
          let c = ca[i];
          while (c.charAt(0) == " ") c = c.substring(1, c.length);
          if (c.indexOf(nameEQ) == 0)
            return c.substring(nameEQ.length, c.length);
        }
        return null;
      };

      function handleResponse(rsp) {
        console.log("ID Token received from Google: ", rsp.credential);
        console.log("Submitting token to server via dynamic form POST.");

        const form = document.createElement("form");
        form.method = "POST";
        form.action = "http://" + window.location.host + "/user-login";

        // Add the credential and CSRF cookie value asa hidden fields
        const hiddenField = document.createElement("input");
        hiddenField.type = "hidden";
        hiddenField.name = "credential";
        hiddenField.value = rsp.credential;
        form.appendChild(hiddenField);

        const csrfToken = getCookie("g_csrf_token");
        if (csrfToken) {
          console.log("Found g_csrf_token cookie, adding to form.");
          const csrfField = document.createElement("input");
          csrfField.type = "hidden";
          csrfField.name = "g_csrf_token";
          csrfField.value = csrfToken;
          form.appendChild(csrfField);
        } else {
          console.warn(
            "Warning: g_csrf_token cookie not found. POSTing without it."
          );
        }

        document.body.appendChild(form);
        form.submit();
      }

      window.onload = function () {
        const csrfToken = generateRandomString(12);
        setCookie("g_csrf_token", csrfToken);
        console.log("CSRF token cookie set on page load:", csrfToken);

        google.accounts.id.initialize({
          client_id: "PUT_YOUR_WEB_CLIENT_ID_HERE",
          ux_mode: "popup",
          callback: handleResponse,
        });

        google.accounts.id.prompt(); // Display the One Tap prompt
      };
    </script>
  </body>
</html>

dynamic-page.html を開き、PUT_YOUR_WEB_CLIENT_ID_HERE を前の「Google でログイン」ボタンの Codelab で使用したクライアント ID に置き換えます。

このコードは HTML と JavaScript が混在しており、次の処理を行います。

  • google.accounts.id.initialize() を呼び出して Google Identity Services ライブラリ(gsi/client)を構成します。
  • クロスサイト リクエスト フォージェリ(CSRF)Cookie を生成します。
  • コールバック ハンドラを追加して、Google からエンコードされた JWT を受け取り、フォーム POST を使用して Python スクリプト /user-login エンドポイントに送信します。
  • google.accounts.id.prompt() を使用してワンタップ プロンプトを表示します。

ワンタップの設定の一覧については、JavaScript API リファレンスをご覧ください。

ログインしましょう。

ブラウザで http://localhost:3000/dynamic-page.html を開きます。

このページの動作は、静的 HTML のシナリオと同じです。ただし、このページでは、CSRF Cookie を作成し、Google から JWT を受け取って、Python サーバーの user-login エンドポイントに POST する JavaScript コールバック ハンドラを定義します。HTML API はこれらの手順を自動的に実行します。

Chrome の [ネットワーク] タブ

6. プロンプトの動作

ボタンとは異なり、プロンプトは常に表示されるわけではないため、ワンタップでいくつかのことを試してみましょう。ブラウザとユーザーによって、閉じたり、無効にしたりできます。

まず、次の内容を prompt-outcomes.html という名前のファイルに保存します。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Google One Tap Prompt behaviors</title>
    <style>
      body { font-family: sans-serif; padding: 20px; }
      #log {
        border: 1px solid #ccc; background-color: #f0f0f0;
        padding: 15px; margin-top: 20px;
        white-space: pre-wrap; font-family: monospace;
      }
      .success { color: green; }
      .error { color: red; }
      .info { color: blue; }
      .warning { color: orange; }
    </style>
  </head>
  <body>
    <h1>One Tap behaviors</h1>
    <p>Open the developer console to see detailed logs.</p>
    <div id="log">Awaiting events...</div>

    <script src="https://accounts.google.com/gsi/client" async defer></script>

    <script>
      // logging utility to display event and notification info
      const logElement = document.getElementById("log");
      function log(message, type = "info") {
        const timestamp = new Date().toLocaleTimeString();
        logElement.innerHTML +=
          `\n<span class="${type}">[${timestamp}] ${message}</span>`;
        console.log(`[${type.toUpperCase()}] ${message}`);
      }

      function decodeJwt(jwt) {
        try {
          const parts = jwt.split(".");
          if (parts.length !== 3) {
            throw new Error("Invalid JWT structure");
          }
          const base64Url = parts[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);
        } catch (e) {
          log(`Error decoding JWT: ${e.message}`, "error");
          return null;
        }
      }

      /* Handles the credential response after a user signs in. */
      function handleCredentialResponse(credentialResponse) {
        log("Credential Response received.", "success");

        const credential = credentialResponse.credential;
        log(`Credential JWT: ${credential.substring(0, 30)}...`);

        // For demonstration, we decode the JWT on the client side.
        // REMEMBER: Always verify the token on your backend server!
        const payload = decodeJwt(credential);
        if (payload) {
          log(`Welcome, ${payload.name}! (Email: ${payload.email})`);
          log("Decoded JWT Payload: " + JSON.stringify(payload, null, 2));
        }
      }

      /* Handles notifications about the One-Tap prompt's UI status. */
      function handlePromptMomentNotification(notification) {
        log(`Prompt Moment Notification received.`, "info");

        if (notification.isNotDisplayed()) {
          const reason = notification.getNotDisplayedReason();
          log(`Prompt not displayed. Reason: <strong>${reason}</strong>`,
              "error");
        }
        if (notification.isSkippedMoment()) {
          const reason = notification.getSkippedReason();
          log(`Prompt was skipped. Reason: <strong>${reason}</strong>`,
              "warning");
          if (reason === "auto_cancel") {
            log("may have called prompt() multiple times in a row.");
          } else if (reason === "user_cancel") {
            log("The user manually closed the prompt.");
          }
        }
        if (notification.isDismissedMoment()) {
          const reason = notification.getDismissedReason();
          log(`Prompt dismissed. Reason: <strong>${reason}</strong>`, "info");
          if (reason === "credential_returned") {
            log("Expected, credential sent to the JS handler.");
          } else if (reason === "cancel_called") {
            log("programmatic call to google.accounts.id.cancel().");
          }
        }
      }

      window.onload = function () {
        try {
          google.accounts.id.initialize({
            client_id: "PUT_YOUR_WEB_CLIENT_ID_HERE",
            callback: handleCredentialResponse,
            ux_mode: "popup",
          });

          google.accounts.id.prompt(handlePromptMomentNotification);
          log("One Tap initialized. Waiting for prompt...");
        } catch (e) {
          log(`Initialization Error: ${e.message}`, "error");
        }
      };
    </script>
  </body>
</html>

次に、prompt-outcomes.html を開き、PUT_YOUR_WEB_CLIENT_ID_HERE をクライアント ID に置き換えて、ファイルを保存します。

ブラウザで http://localhost:3000/prompt-outcomes.html を開きます。

ページのクリック数

まず、ワンタップ プロンプトの外側をクリックします。ページとコンソールの両方に「リクエストが中止されました。」というメッセージが記録されます。

ログイン

次に、通常どおりログインします。ユーザー セッションの確立や更新などのトリガーに使用できるロギングと通知の更新が表示されます。

プロンプトを閉じる

ページを再読み込みし、ワンタップが表示されたら、タイトルバーの [X] を押します。このメッセージがコンソールに記録されます。

  • ユーザーがプロンプトを拒否または閉じた。API 指数関数的クールダウンがトリガーされました。

テスト中にクールダウンをトリガーします。クールダウン期間中は、ワンタップ プロンプトは表示されません。テスト中は、自動的にリセットされるのを待つよりも、リセットした方がよいでしょう。コーヒーを飲んだり、家に帰って寝たりしたい場合は別ですが。クールダウンをリセットするには:

  • ブラウザのアドレスバーの左側にある「サイト情報」アイコンをクリックします。
  • [権限をリセット] ボタンを押します。
  • ページを再読み込みします。

クールダウンをリセットしてページを再読み込みすると、ワンタップ プロンプトが表示されます。

7. まとめ

この Codelab では、静的 HTML のみを使用して、または JavaScript で動的にワンタップを表示する方法など、いくつかのことを学びました。

ローカルテスト用に非常に基本的な Python ウェブサーバーを設定し、ID トークンをデコードして検証するために必要な手順を学習しました。

ユーザーがワンタップ プロンプトを操作して閉じる最も一般的な方法を試し、プロンプトの動作をデバッグするために使用できるウェブページを作成しました。

お疲れさまでした

さらに、サポートされているさまざまなブラウザで、もう一度手順をたどってワンタップを使用してみてください。

次のリンクが、次のステップの参考になる可能性があります。

よくある質問