Zenn
🔑

PHP8でGoogleログインを実装する【Google API/OAuth2.0/PHP8】

2025/03/21に公開

課題

PHP8系プロジェクトにおけるGoogeログインの実装について、ぱっと参照できる答えが欲しい。

キーワード

  • Google Cloud Platform
  • Google Login(Google Oauth)
  • PHP8

手続き

  • OAuth2.0を理解
  • 専用のSDKをComposerでプロジェクトにインストール
  • 下準備
    • GCPでプロジェクトを作成(既存プロジェクトでもOK)
    • 「OAuth同意画面」のAPIを有効化
    • コールバック用のリダイレクトURLなどを登録
    • CLIENT_IDやCLIENT_SECRETキーを取得
  • 同意リクエストを開始するボタンをHTMLで作成(公式生成機能あり)
    • ボタン生成公式ツールベタ書きではなく、PHPで動的に書く場合
  • POSTリクエストをバックエンド側で処理してトークンを受信
  • SDKからオブジェクトを呼び出して、CLINET_IDを照合
  • verifyIdToken()メソッドでペイロードを受信
  • ペイロードから必要なデータを取得

すべてデモコードがあるのでとくにむずかしいところはないと思います。

公式による概要はこちら。

OAuth2.0を理解

おもいっきり割愛。

あまり人道的ではない学習方法ですが、攻撃者側の気持ちになると理解が深まります。stateの不備や、codeの管理とか、Refererヘッダまわりとか、redirect_uri検証不足とか。

専用のSDKをComposerでプロジェクトにインストール

composer require google/apiclient

下準備

このあたりの下準備については割愛します。

GCPでプロジェクトを作成

「OAuth同意画面」のAPIを有効化

コールバック用のリダイレクトURLなどを登録

CLIENT_IDやCLIENT_SECRETキーを取得

みなさんのブログ記事がスクリーンショット豊富でわかりやすいです。

同意リクエストを開始するボタンをHTMLで作成(公式生成機能あり)

よくみる「Googeでログイン」みたいなボタンのHTMLを生成できる公式ツールです。使わなくてもぜんぜんつくれますが、いちおうご紹介。

ボタン生成公式ツールベタ書きではなく、PHPで動的に書く場合

環境変数の設定

だいたい.envとかに環境変数おくと思うのでセットしておきます。

.env
GOOGLE_OAUTH_CLIENT_ID={クライエントID}
GOOGLE_OAUTH_REDIRECT_URL={リダイレクトURL}

環境変数のために、PHP dotenvをインストールします。

composer require vlucas/phpdotenv

HTML側の実装

Googleログインボタン側のHTMLです。わたしはscript要素をhead要素のなかに入れたいので、移動させています。async属性かdefer属性か迷いどころですが、公式どおりにしておきます。

id属性がg_id_onloaddivタグが公式でもにありました。data属性にいろいろデータを入力することで、ボタンが完成します。GCPのプロジェクト側から払い出されたCLIENT_IDや、じぶんのところのアプリのコールバックURLを属性の値にいれます。

auth/google-oauth-request/
<?php
// オートローダー
require_once __DIR__.'/vendor/autoload.php';

// 環境変数のセッティング(先ほどの.envを読み込んで、$_ENVに入れる)
$dotenv = Dotenv\Dotenv::createImmutable();
$dotenv->load();
?>

...

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

...

<section>
<div id="g_id_onload"
    data-client_id="<?= h($_ENV['GOOGLE_OAUTH_CLIENT_ID']) ?>"
    data-login_uri="<?= h($_ENV['GOOGLE_OAUTH_REDIRECT_URI']) ?>"
    data-auto_prompt="false">
</div>
<div class="g_id_signin"
    data-type="standard"
    data-size="large"
    data-theme="outline"
    data-text="sign_in_with"
    data-shape="rectangular"
    data-logo_alignment="left">
</div>
</section>

ドキュメントはこの部分です。

data属性のリストはこちらにあります。いろいろカスタム効きます。

フロント側はこれだけでOKです。

POSTリクエストをバックエンド側で処理してトークンを受信

PHP用のクライアントライブラリはこちらから。

コードとしてはこうなります。

auth/google-oauth-callback/
<?php

/**
・このファイルはGCPプロジェクトの同意画面APIで入力したコールバックURLで指定したURL
・auth/google-oauth-request/のグーグルログインボタンを押したあとのポップアップから遷移
*/

// オートローダー
require_once __DIR__.'/vendor/autoload.php';

// 環境変数のセッティング(先ほどの.envを読み込んで、$_ENVに入れる)
$dotenv = Dotenv\Dotenv::createImmutable();
$dotenv->load();

// $_POSTで送られてくるクレデンシャルを受信して、verifyIdToken()メソッドでGoogleアカウントのデータを取得する

try {
    if(!isset($_POST["credential"])) throw new \Exception("credentialが存在しません");

    $credential = $_POST["credential"];
    $client = new Google_Client(['client_id' => $_ENV['GOOGLE_OAUTH_CLIENT_ID']]);
    $payload = $client->verifyIdToken($credential);
    $iss = $payload["iss"]; // Issuer
    $azp = $payload["azp"]; // Authorized party
    $aud = $payload["aud"]; // Audience
    $sub = $payload["sub"]; // Subject
    $email = $payload["email"]; // Email
    $email_verified = $payload["email_verified"]; // Email verified
    $nbf = $payload["nbf"]; // Not before
    $name = $payload["name"]; // Name
    $picture = $payload["picture"]; // Picture
    $given_name = $payload["given_name"]; // Given name
    $iat = $payload["iat"]; // Issued at
    $exp = $payload["exp"]; // Expiration
    $jti = $payload["jti"]; // JWT ID

    // バリデーション
    if ($iss !== 'https://accounts.google.com' && $iss !== 'accounts.google.com') throw new \Exception("Invalid issuer.");
    if ($azp !== $_ENV['GOOGLE_OAUTH_CLIENT_ID']) throw new \Exception("Invalid client ID.");
    if ($aud !== $_ENV['GOOGLE_OAUTH_CLIENT_ID']) throw new \Exception("Invalid audience.");
    if (!$email_verified) throw new \Exception("Invalid email verified.");
    if ($nbf > time()) throw new \Exception("Invalid not before.");
    if ($iat > time()) throw new \Exception("Invalid issued at.");
    if ($exp < time()) throw new \Exception("Invalid expiration.");

    // なにかしらの処理

    // ログイン処理

} catch (\Exception $e) {
    echo $e->getMessage();
}

まとめ

ComposerやSDKのおかげで実装自体は簡単ですが、技術全体を理解するには長い道を歩く必要がありそうです。

Discussion

ログインするとコメントできます