(Cake)PHP で Sign in with Google を実装した時のメモ

(※この記事は 別媒体に投稿した記事 のバックアップです。 canonical も設定しています)

2020-11-22

※この記事は別アカウント(hyiromori)から引っ越しました

はじめに

社内ツールに認証を入れる際に、Google OAuth を使った認証の仕組みをいれました。
その作業の記録です。

CakePHP 4.1.6 を使っていますが、基本的な部分は CakePHP に限らず PHP 全般で使えると思います。

また最近OAuthなど認証の学習を始めたので、違う箇所や別の良い方法がある場合はコメントいただけるとありがたいです。

前提

  1. GCPで認証情報は作成され、JSONファイルを取得済であること
  2. OAuth2 の Authorization Code Grant によるフローで実装しています

やりたいこと

今回は Google OAuth を使ってユーザーを認証することを目的としていました。
またGoogleアカウントに紐づくメールアドレス、名前、プロフィール画像を取得してアカウント情報を作ることもしました。

認証のためのライブラリの導入

まずGoogle が提供しているライブラリを導入しました。
googleapis/google-api-php-client: A PHP client library for accessing Google APIs

このドキュメントに紹介されています。
Using OAuth 2.0 for Web Server Applications  |  Google Identity Platform

インストール

composer で一発でした。

composer require google/apiclient:"^2.7"

認証情報の設定

GCPで認証情報を作成すると、JSONファイルで認証情報をダウンロードできます。

今回はJSON文字列ごと環境変数に設定して、それをパースして使用する方法にしました。

## こんな感じで設定されているイメージです
$ export GOOGLE_AUTH_CONFIG='{"web":"...(略)..."}'
// こんな感じで環境変数をパースして使用します。
$config = json_decode(env('GOOGLE_AUTH_CONFIG', '{}'), true);

※以降のサンプルコード内の $config という変数は、このパースした認証情報を指します

APIクライアントの生成

APIクライアントの生成は、以下のように実行します。

$client = new Client();

// 上記のパースした認証情報をセットする
$client->setAuthConfig($config);

// リダイレクト用のURLをセットする
// GCPで設定したものと同じである必要がある
$client->setRedirectUri('http://localhost:8080/callback');

※以降のサンプルコード内の $client という変数は、このAPIクライアントを指します

エンドポイントの実装

次に認証関連のエンドポイントを用意しました。
必要になるエンドポイントは3つです。

/sign_in

サインイン画面を表示するためのエンドポイントです。
特に処理とかはなく、次に出てくる /request_authorize へのリンクを設置するだけです。

こんな感じで Sign in with Google のリンクを押して、リンクは /request_authorize を指定しています。

/sign_in

(アイコンは以下のリンクにあります)
ログインにおけるブランドの取り扱いガイドライン  |  Google Identity Platform  |  Google Developers

/request_authorize

Google の OAuth エンドポイントへリダイレクトするエンドポイントです。
色々とパラメーターを付与してリダイレクトする必要があります。

リダイレクト時に付与するパラメーター

リダイレクトする際に、以下の情報をクエリパラメーターに追加しておきます。

CakePHPでの実装例

CakePHP のコントローラーはこんな感じで実装しました。

public function requestAuthorize()
{
    // state の生成
    $state = base64_encode(random_bytes(16));

    $query = http_build_query([
        'client_id' => $config['web']['client_id'], // 認証情報からクライアントIDを取得
        'redirect_uri' => 'http://localhost:8080/callback', // GCPで設定したものと同じである必要がある
        'response_type' => 'code',
        'scope' => 'openid email profile',
        'access_type' => 'offline',
        'state' => $state
    ]);

    // callback で state のチェックをするためにセッションにセットしておく
    $this->getRequest()->getSession()->write('oauth_state', $state);

    // Google のエンドポイントへリダイレクト
    $this->redirect("https://accounts.google.com/o/oauth2/v2/auth?{$query}");
}

stateCSRF を防ぐために使用するものです。
以下の記事がとても分かりやすかったです。

OAuthやOpenID Connectで使われるstateパラメーターについて | SIOS Tech. Lab

/callback

Googleでの認証が終わった後に呼ばれるエンドポイントです。
Gクエリパラメーターに必要なパラメーターが付与されているので、それらを処理して認証を完了します。

このエンドポイントはやることが多いので、軽く流れを説明します。

  1. クエリパラメーターを取得する
  2. state のチェックし一致しない場合は処理を終了する
  3. Googleからアクセストークンを取得する
  4. IDトークンを検証して認証する
  5. サインイン完了の処理をする
クエリパラメーターを取得する

以下2つのパラメーターが付与されているので取得します。

state のチェックし一致しない場合は処理を終了する

クエリパラメーターの state と、セッションの state が一致するかを検証します。
これによって、正しいリクエストであることを検証できます。

Googleからアクセストークンを取得する

クエリパラメーターに付与されている code は Google にアクセスして情報を取得するための引換券のようなものです。
code を使って、アクセストークンを取得します。
APIクライアントのメソッドを使えば一行でできます。

$code = $this->getRequest()->getQuery('code');
$accessToken = $client->fetchAccessTokenWithAuthCode($code);
IDトークンを検証して認証する

アクセストークン内にある id_token というトークンを検証します。
このIDトークンを検証することで、ユーザーの認証が完了します。
こちらもAPIクライアントのメソッドを使えば一行でできます。

$userInfo = $client->verifyIdToken($accessToken['id_token']);
if (!$userInfo) {
    return $this->renderError('トークンの検証に失敗しました');
}
サインイン完了の処理をする

ここは各アプリケーションの要件によって実装が変わってきます。
私の場合は、以下のような処理をしました。

  1. メールアドレス、名前、プロフィール画像を取得
  2. ユーザー情報の作成 or 更新
  3. セッションにユーザー情報を入れる
  4. 初期画面にリダイレクト

参考までに $userInfo から以下のように情報を所得できます。

$email = $userInfo['email'];
$name = $userInfo['name'];
$picture = $userInfo['picture'];

おわりに

Google OAuth を使うのは、思っていたよりも手軽にできることが分かりました。
とはいえ、何のためにこの処理を書いているのかは分かったほうが良いなー、と思いました。

私は認証認可の学習をしていて、こういった実装をしてみることで結構理解が深まった気がします。
認証認可は結構重要かつ色々なプロダクトで使っていくものなので、しっかり学習していきたいですね。

疑問(自分への宿題)

Google は OAuth と言っているけど、OpenID Connect とは何が違うのかな・・・?
OAuth2 の上に OpenID Connect が定義されている、とは見たけれど、どう違うのかは分かっていない。
この辺は学習を進めてわかったら追記するかもしてません。