Googleのソーシャルログイン用のJSライブラリが新しくなったので対応する(Sign In With Google / Google Identity Services API)

# 概要
2021年8月、Googleのソーシャルログイン用のJSライブラリが変わることが発表されました。

[Google Developers Japan: 新しい Google Identity Services API のリリースについて](https://developers-jp.googleblog.com/2021/08/google-identity-services-api.html)
[Google Developers Japan: ウェブ用の Google Sign-In JavaScript プラットフォーム ライブラリの提供終了について](
https://developers-jp.googleblog.com/2021/09/google-sign-in-javascript.html)

詳しくはこちらの記事をご覧いただければと思うのですが、大きなポイントとしては

- 使えなくなるのはブラウザ用JSライブラリ。**2023年3月でダウンロードできなくなる。** 旧サンプルだとscriptタグでライブラリに直リン（apis.google.com/js/platform.js）してるはずなので、それが使えなくなる。
- 「Google Identity Services」APIを利用した新ライブラリへ移行
- ソーシャルログイン周りの機能名（コーナー名／ブランド名）が「Google Sign-In for WebSites」から「Sign In With Google」へと変更。

というような形になります。3番目の名称変更はどうでもいい……ように見せかけて、移行ドキュメントを読む際は意識してないと、怪しい日本語訳と相まって新旧ごっちゃになってしまうので、頭の片隅に置いておいて下さい。


# 実装
クライアントIDの設定周りも設定項目が増えているのですが、[マニュアル](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid)を見れば何とかなるかと。
大きな変更点として、旧方式では、認証後にコールバックに設定した関数の引数にGoogleユーザーの情報がオブジェクトとして渡されたと思うんですが、**新方式では、旧方式で言うところのIDトークン=[JWT](https://ja.wikipedia.org/wiki/JSON_Web_Token)ぐらいしか返ってきません。**
これをそのままバックエンドに送ってGoogle製のライブラリを使って検証してから使うという形になります。おそらく旧方式だと、Googleユーザーの情報をそのままバックエンドに送って、検証せずに使っちゃう事例が多かったんでしょうね……😑
ただ、**訳の分からない文字列では味気が無い、どうしてもフロントエンド側でGoogleユーザーの情報を確認して、認証できてる実感が欲しい……**という人もいるかと思います。バックエンド担当が別の人の場合とか。JWTについて調べてもらったら分かると思うんですが、ユーザー情報や署名を含むJSONをBase64urlでエンコードしただけなので、デコードすればユーザー情報を見ることができます。
デコードのやり方については自前でもできますが、有名認証プラットフォームであるauth0が運営する[jwt.io](https://jwt.io/)にJWTデバッガーがあります。レスポンスのcredentialプロパティの文字列を張り付けることで元のJSONを見ることができます。いわゆるパスワードチェッカー系と同じでWebに貼ったら漏れるのでは……と心配でしたら、[jwt-decode](https://github.com/auth0/jwt-decode)というOSSライブラリが検証抜きでいきなりデコードできるので、今回の用途向きです。まぁこれもauth0が提供してるんですが😅
最後に、繰り返しになりますが、**JWTはサーバーサイドに送って検証してから使ってください。**

# おまけ：TypeScript用の型定義
新しいライブラリ周りの型を[リファレンス](https://developers.google.com/identity/gsi/web/reference/js-reference)からコピペしながら起こしたものです。リファレンス自体に間違いと思われる点があるので、追加で注釈を入れてあります。ESLintは通るようにしましたが、実際に型定義が正しいか検証していないため、もし間違ってたら修正願います。

まず認証後のレスポンスの型になります。各自のアプリケーション内で普通に定義して使います。

```ts
type SelectBy =
  // 以前に資格情報の共有に同意した既存のセッションを持つユーザーの自動サインイン。
  "auto" |
  // 以前に同意を付与した既存のセッションを持つユーザーは、ワンタップの[続行]ボタンを押して資格情報を共有しました。
  "user" |
  // 既存のセッションを持つユーザーがワンタップの[続行]ボタンを押して、同意を付与し、資格情報を共有しました。 
  // Chromev75以降にのみ適用されます。
  "user_1tap" |
  // 既存のセッションを持たないユーザーは、ワンタップの[続行]ボタンを押してアカウントを選択し、
  // ポップアップウィンドウの[確認]ボタンを押して同意を付与し、資格情報を共有します。
  // Chromiumベース以外のブラウザに適用されます。
  "user_2tap" |
  // 以前に同意を付与した既存のセッションを持つユーザーは、[Googleでサインイン]ボタンを押し、
  // [アカウントの選択]からGoogleアカウントを選択して資格情報を共有しました。
  "btn" |
  // 既存のセッションを持つユーザーが[Googleでサインイン]ボタンを押し、[確認]ボタンを押して、同意を付与し、
  // 資格情報を共有しました。
  "btn_confirm" |
  // 以前に同意を付与した既存のセッションのないユーザーは、[Googleでサインイン]ボタンを押してGoogleアカウントを選択し、
  // 資格情報を共有しました。
  "btn_add_session" |
  // 既存のセッションを持たないユーザーは、最初に[Googleでサインイン]ボタンを押してGoogleアカウントを選択し、
  // 次に[確認]ボタンを押して同意し、資格情報を共有します。
  "btn_confirm_add_session"

type CredentialResponse = {
  // 旧IDトークン=JWT
  credential: string,
  select_by: SelectBy
  // 注：リファレンスに載ってないが、実際はあるのでOptional扱い
  clientId?: string,
}
```

次にScriptタグで読み込むことになるライブラリ内の型です。これらの型は「グローバル変数として」宣言する必要があります。この手順に関しては、[こちらの記事](https://dev.classmethod.jp/articles/typings-of-window-object/)が参考になります。

```ts
/**
 * グーグルのサインイン用ライブラリの型.
 * 
 * @link https://developers.google.com/identity/gsi/web/reference/js-reference
 */
type IdConfiguration = {
  // アプリケーションのクライアントID
  client_id: string,
  // 自動選択を有効にします。
  auto_select?: boolean,
  // IDトークンを処理するJavaScript関数。 
  // GoogleのひとつをタップしてGoogleボタンサインインpopup UXモードでは、この属性を使用します。
  callback: function,
  // ログインエンドポイントのURL。でGoogleボタンサインインredirect UXモードでは、この属性を使用しています。
  login_uri?: string,
  // パスワードクレデンシャルを処理するJavaScript関数。
  native_callback?: function,
  // ユーザーがプロンプトの外側をクリックした場合、プロンプトをキャンセルします。
  cancel_on_tap_outside?: boolean,
  // ワンタッププロンプトコンテナ要素のDOMID
  prompt_parent_id?: string,
  // IDトークンのランダムな文字列
  nonce?: string,
  // ワンタッププロンプトのタイトルと単語
  context?: "signin" | "signup" | "use",
  // 親ドメインとそのサブドメインでワンタップを呼び出す必要がある場合は、
  // 親ドメインをこのフィールドに渡して、単一の共有Cookieが使用されるようにします。
  state_cookie_domain?: "string",
  // サインインとGoogleボタンのUXフロー
  ux_mode?: "popup" | "redirect"
  // 中間iframeの埋め込みが許可されているオリジン。
  // このフィールドが表示されている場合、ワンタップは中間iframeモードで実行されます。
  allowed_parent_origin?: string | Array<string>,
  // ユーザーが手動でワンタップを閉じると、デフォルトの中間iframe動作が上書きされます。
  intermediate_iframe_close_callback?: function
}

type GsiButtonConfiguration = {
  // ボタンの種類：アイコン、または標準ボタン。
  // リファレンスの表では必須となっているが、サンプル等はなしで動く
  type?: "standard" | "icon",
  // ボタンのテーマ。たとえば、filled_blueまたはfilled_blackです。
  theme?: "outline" | "filled_blue" | "filled_black",
  // ボタンのサイズ。たとえば、小さいまたは大きい。
  size?: "large" | "medium" | "small",
  // ボタンのテキスト。たとえば、「Googleでログイン」や「Googleで登録」などです。
  // 注：公式リファレンスに"signup_with"が2つ載っている
  text?: "signin_with" | "signup_with" | "continue_with" | "signup_with",
  // ボタンの形。たとえば、長方形または円形。
  shape?: "rectangular" | "pill" | "circle" | "square",
  // Googleロゴの配置：左または中央。
  logo_alignment?: "left" | "center",
  // ボタンの幅（ピクセル単位）。
  // 注: stringになっているが合っているか不明
  width?: string,
  // 設定されている場合、ボタンの言語がレンダリングされます。
  locale?: string
}

type PromptMomentNotification = {
  isDisplayMoment: () => boolean,
  isDisplayed: () => boolean,
  isNotDisplayed: () => boolean,
  getNotDisplayedReason: () => "browser_not_supported" | "invalid_client" | "missing_client_id" | "opt_out_or_no_session" | "secure_http_required" | "suppressed_by_user" | "unregistered_origin" | "unknown_reason"
  isSkippedMoment: () => boolean,
  getSkippedReason: () => "auto_cancel" | "user_cancel" | "tap_outside" | "issuing_failed",
  isDismissedMoment: () => boolean,
  getDismissedReason: () => "credential_returned" | "cancel_called" | "flow_restarted",
  getMomentType: () => "display" | "skipped" | "dismissed"
}

type Credential = {
  id: string,
  password: string,
}

type RevocationResponse = {
  // このフィールドは、メソッド呼び出しの戻り値です。
  successful: boolean,
  // このフィールドには、オプションで詳細なエラー応答メッセージが含まれます。
  error?: string,
}

type id = {
  initialize: (config: IdConfiguration) => void,
  prompt: (momentListener: (notification: PromptMomentNotification) => void) => void,
  renderButton: (parent: HTMLElement, options: GsiButtonConfiguration) => void,
  disableAutoSelect: () => void,
  storeCredential: (credential: Credential, callback?: function) => void,
  cancel: () => void,
  revoke: (hint: string, callback: (done: RevocationResponse) => void) => void
}

interface Window {
  google: {
    accounts: {
      id: id
    }
  },
  onGoogleLibraryLoad: () => void
}
```