5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

OpenIDconnectを用いたGoogleアカウントのソーシャルログインを実装してみる。

Last updated at Posted at 2021-07-04

Index

初めに

ソーシャルログインが最低限動く環境を作ってみました。
シンプルな造りなのでソーシャルログインについての概要は理解しやすいかと思います。

でも、セキュリティを担保できるほどの試験はしてないし、
実際に商用でのサービス提供もしていないので、これをそのまま商用で使うのはちょっと心許ないです。
あと、商用で使うならGCP側の設定を商用向けの設定にしないといけないっぽいです。

ちなみにfacebookアカウントを使ったソーシャルログイン環境の構築手順もまとめてますので、
興味のある方はそちらもどうぞ。

必要条件

  • googleアカウント(事前に作成済である前提としています。)
  • GCP環境が利用できる状態。
  • phpが動作するウェブサーバ環境(本記事ではXAMPPでlocalhost環境を使ってますが、LAMPでも動きます。)
    ※XAMPP環境の構築をする場合はこちらを参考にしてください。

全体概要図

ざっくりとした全体の処理の流れです。
概要図.jpg

構築手順

連携アプリ側の設定

  • Apacheの制御下にあるディレレクトリ(phpが動作する環境)へ、以下の様にファイル配置していきます。
C:\xampp\htdocs (←Apacheの制御下のフォルダ、環境の設定によって変わる)
      └ social-login
          └ google
              ├ index.php
              ├ common.php
              ├ redirect.php
              └ btn_google_signin_dark_normal_web.png

btn_google_signin_dark_normal_web.pngは、Googleのページからダウンロードしてきたボタン画像のファイルです。ファイルをダウンロードして解凍してそのまま配置してます。

  • index.phpはログインのためのボタンを表示してるだけのシンプルな画面のphpソースコードです。
index.php
<html>
<head>
    <meta charset="utf-8">
    <title>Demo sign in with Google</title>
</head>
<body>
    <?php 
    require_once __DIR__."/common.php";

    $url = "https://accounts.google.com/o/oauth2/auth";
    $param = http_build_query(array(
        "client_id" => $clientId,
        "redirect_uri" => $redirectUri,
        "scope" => "openid email",
        "response_type" => "code"
    ));
    echo '<a href="' . $url . "?" . $param . 
    '"><img src="btn_google_signin_dark_normal_web.png" alt="googleIcon" title="googleIcon"></a>';
    ?>
</body>
</html>
  • common.phpはこのデモアプリ全体で使用するパラメータの設定と、使用頻度の高いbase64のデコード/エンコード関数を定義するためのファイルです。
    後ほど、Google側の設定をした後に、ここの $clientId$clientSecret$redirectUri の値を設定します。
common.php
<?php
$clientId = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com";
$clientSecret = "XXXXXXXXXXXXXXXXXXXXXXXX";
$redirectUri = "https://localhost/social-login/google/redirect.php";

function webSafeBase64encode($data){
  return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function webSafeBase64decode($data){
  return base64_decode(strtr($data, '-_', '+/') . str_repeat('=', 3 - (3 + strlen($data)) % 4));
}
  • redirect.phpは、受け取った認可コードを使ってgoogleにアクセストークンやIDトークン(ユーザ情報含む)をリクエストしてます。また、受け取った情報を検証したりもしています。
redirect.php
<?php
require_once __DIR__ . "/common.php";

if (!array_key_exists("code", $_GET)) {
	echo "<hr>Error : incorrect parameter";
	return;
}

$param = http_build_query(array(
	"code" => $_GET["code"],
	"client_id" => $clientId,
	"client_secret" => $clientSecret,
	"redirect_uri" => $redirectUri,
	"grant_type" => "authorization_code"
));

$context = array(
	"http" => array(
		"method"  => "POST",
		"header"  => "Content-Type: application/x-www-form-urlencoded\r\nContent-Length: " . strlen($param),
		"content" => $param
	)
);

//Googleにリクエスト
$url = "https://accounts.google.com/o/oauth2/token";
$resp = json_decode(file_get_contents($url, false, stream_context_create($context)));


echo "<h3>*** Response ***</h3>";
echo var_export($resp, true);

//id token(JWT [ヘッダー.ペイロード.署名])をピリオドで分離
$jwt = explode(".", $resp->id_token);

//ヘッダー部分を解析
$header = json_decode(webSafeBase64decode($jwt[0]));
echo "<hr><h3>*** Header ***</h3>";
echo var_export($header, true);

if (strcasecmp($header->typ, "JWT")!== 0) {
	//JWT形式のみ対応とします。
	echo "<hr>Error : Unsupported format";
	return;
}

$alg = "";
switch ($header->alg) {
	case "RS256"://RSA PKCS#1 signature with SHA-256
	case "PS256"://RSA PSS signature with SHA-256
		$alg = OPENSSL_ALGO_SHA256;
		break;
	case "RS384"://RSA PKCS#1 signature with SHA-384
	case "PS384"://RSA PSS signature with SHA-384
		$alg = OPENSSL_ALGO_SHA384;
		break;
	case "RS512"://RSA PKCS#1 signature with SHA-512
	case "PS512"://RSA PSS signature with SHA-512
		$alg = OPENSSL_ALGO_SHA512;
		break;
	default:
		echo "<hr>Error : Unsupported algorithm";
		return;
}

//公開鍵をダウンロード
$certs = (array)json_decode(file_get_contents("https://www.googleapis.com/oauth2/v1/certs"));
foreach ($certs as $keyId => $pem) {
	if (strcasecmp($keyId, $header->kid) === 0) {
		//署名検証
		$verifyed = openssl_verify($jwt[0] . "." . $jwt[1], webSafeBase64decode($jwt[2]), $pem , $alg);
		if ($verifyed == 1) {
			break;
		}else{
			echo "<hr>Error : Signature Verification Failed " . openssl_error_string();
			return;
		}
	}
}

//ペイロード部分を解析
$payload = json_decode(webSafeBase64decode($jwt[1]));
echo "<hr><h3>*** Payload ***</h3>";
echo var_export($payload, true);


//RFC7519で定義されている必須パラメータが有るかどうかをチェック
if (
	!property_exists($payload, "iss") ||
	!property_exists($payload, "sub") ||
	!property_exists($payload, "aud") ||
	!property_exists($payload, "exp") ||
	!property_exists($payload, "iat")
) {
	echo "<hr>Error : Invalid Param Received";
	return;
}

//レスポンス発行者をチェック
if (
	$payload->iss !== "https://accounts.google.com" &&
	$payload->iss !== "accounts.google.com"
) {
	echo "<hr>Error : Invalid Issure";
	return;
}

//今回の使い方だとクライアントIDは一致する。使い方によっては一致しない場合もあるらしい。
if (
	$payload->azp !== $clientId ||
	$payload->aud !== $clientId
){
	echo "<hr>Error : Mismatch Client Id";
	return;
}

//access token のhash値を検証する。
$hashedAccessToken = hash("sha256",$resp->access_token);
$hexHash =  bin2hex(webSafeBase64decode($payload->at_hash));
if (substr($hashedAccessToken, 0, strlen($hexHash)) !== $hexHash){
	echo "<hr>Error : Invalid Access token";
	return;
}

//ユーザのEメールアドレスが検証済かどうかをチェックする
if (!$payload->email_verified){
	echo "<hr>Error : Not Email Verified";
	return;
}

//発行日時をチェックする。前後5分くらいのズレを許容するくらいで十分だと思う。
$now = time();
if ((int) $payload->iat < $now - 5 * 60  ||  $now + 5 * 60 < (int) $payload->iat) {
	echo "<hr>Error : Issued at the wrong time";
	return;
}

//ユーザ情報等を画面上に表示する
echo "<hr><h3>*** ユーザ情報等 ***</h3>";
echo "<p>■sub(ユーザの識別子)<br />{$payload->sub}</p>";
echo "<p>■email(ユーザのEメールアドレス)<br />{$payload->email}</p>";

date_default_timezone_set('Asia/Tokyo');
echo "<p>■iat(ID token の発行日時)<br />UnixTime : {$payload->iat} (" .date('Y/m/d H:i:s', $payload->iat) .")</p>";
echo "<p>■exp(ID token の有効期限日時)<br />UnixTime : {$payload->exp} (" .date('Y/m/d H:i:s', $payload->exp) .")</p>";


//認証がOKなら、必要に応じてこれらのパラメータを利用してログイン処理を実装してください。
/**実装例**
$_SESSION['userId'] = $payload->sub;
$_SESSION['email'] = $payload->email;
*/
  • これらのファイルを配置し終わったらhttps://localhost/social-login/google/index.php(ここではlocalhost環境で動かしてます)にアクセスしてみてください。
    以下の様にボタンが1つだけ表示されたページが閲覧できればOKです。
    ログイン画面.jpg

Google側の設定

OAuth同意画面の設定

  • GCPのコンソールにアクセスして、
    メニューから[APIとサービス]>[OAuth同意画面]を選択します。
    ※既に作成済なら、この[OAuth同意画面の設定]の手順は不要です。読み飛ばして認証情報の設定へと進んでください。
    OAuth同意画面.jpg

  • とりあえずここでは「外部」を選択して作成します。
    OAUth同意画面_内部_外部.jpg

  • とりあえず必須項目だけ埋めてしまいます。その他の項目は必要に応じて設定してください。
    設定したら[保存して次へ]をクリック。
    必須項目1.jpg必須項目2.jpg

  • [スコープを追加または削除]をクリックして、選択肢から「opneid」を追加します。
    設定したら[保存して次へ]をクリック。
    スコープの追加.jpg

  • 「③テストユーザ」と「④概要」については特に設定せず、そのまま[保存して次へ]をクリック。
    テストユーザ_概要.jpg

認証情報の設定

  • GCPのコンソールにアクセスして、
    メニューから[APIとサービス]>[認証情報]を選択します。
    OAuth同意画面の設定が未設定の場合は先にそちらを設定してください。
    GCP認証情報.png

  • "OAuthクライアントID"で、認証情報を作成します。
    ※作成初回時は画面のUIが本記事記載の内容とはちょっと違うかもしれません。

OAuthクライアントID.png

  • [アプリケーションの種類]は[ウェブアプリケーション]を選択します。

webアプリケーションを選択.png

  • 詳細を設定していきます
  • "名前"欄に適当な名前を設定します。
    ※ここでは「DemoClient」としています。
  • "承認済みリダイレクトURI"にredirect.phpへのURIアドレスを設定します。
    ※ここではlocalhostの環境に合わせてhttps://localhost/social-login/google/redirect.phpと設定しています。

OAuthクライアントIDの作成.jpg

  • 認証情報が作成できました。
    作成完了時にクライアントIDクライアントシークレットが表示されますので、
    これらの値をcommon.phpの変数である$clientId$clientSecretに設定します。
    ※この画面を見逃しても、こららの値は後から確認することできます。クライアントシークレットはリセットもできます。

クライアントIDとクライアントシークレット.png

  • 一覧に作成した認証情報が追加されてます。

認証情報が作成できた.jpg

動かしてみる

  1. XAMPPを起動する(既に起動している場合は不要)
  2. https://localhost/social-login/google/index.phpにウェブブラウザでアクセスする。
    ログイン画面.jpg
  3. [Sign in with Google]のアイコンをクリックする。
  4. Googleの認証画面に遷移するので、認証する。
    ログイン認証画面.jpg
  5. 認証が成功するとhttps://localhost/social-login/google/redirect.phpにリダイレクトされる。このページではユーザ情報などを表示しています。
    ユーザ情報.jpg

以上

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?