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

## Index
* [初めに](#初めに)
* [必要条件](#必要条件)
* [全体概要図](#全体概要図)
* [構築手順](#構築手順)
    * [連携アプリ側の設定](#連携アプリ側の設定)
    * [Google側の設定](#google側の設定)
        * [OAuth同意画面の設定](#oauth同意画面の設定)
        * [認証情報の設定](#認証情報の設定)
* [動かしてみる](#動かしてみる)

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

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

ちなみに[facebookアカウントを使ったソーシャルログイン環境の構築手順](https://qiita.com/yamori4/items/468fdc98ff58aae0bfeb)もまとめてますので、
興味のある方はそちらもどうぞ。

## 必要条件
* googleアカウント(事前に作成済である前提としています。)
* GCP環境が利用できる状態。
* phpが動作するウェブサーバ環境(本記事ではXAMPPでlocalhost環境を使ってますが、LAMPでも動きます。)  
※XAMPP環境の構築をする場合は[こちら](https://github.com/yamori4/objectOriented/blob/master/_readMe/installXampp.md)を参考にしてください。

## 全体概要図
ざっくりとした全体の処理の流れです。
![概要図.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/c41430b8-339b-cc09-a3a0-6bd79e90b52c.jpeg)



## 構築手順
### 連携アプリ側の設定
* Apacheの制御下にあるディレレクトリ(phpが動作する環境)へ、以下の様にファイル配置していきます。

```
C:\xampp\htdocs (←Apacheの制御下のフォルダ、環境の設定によって変わる)
　　　　　　└ social-login
    　　　　　　└ google
    　   　　　　 　├ index.php
    　　　   　　 　├ common.php
    　　　　　    　├ redirect.php
    　　　　　    　└ btn_google_signin_dark_normal_web.png
```
<b>btn_google_signin_dark_normal_web.png</b>は、[Googleのページ](https://developers.google.com/identity/branding-guidelines?hl=ja)からダウンロードしてきたボタン画像のファイルです。ファイルをダウンロードして解凍してそのまま配置してます。

* 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側の設定をした後に、ここの <b>\$clientId</b>、<b>\$clientSecret</b>、<b>\$redirectUri</b> の値を設定します。

```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](https://localhost/social-login/google/index.php)(ここではlocalhost環境で動かしてます)にアクセスしてみてください。  
以下の様にボタンが1つだけ表示されたページが閲覧できればOKです。
![ログイン画面.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/8fdba9ee-097f-7297-d6f8-db1668b9f9de.jpeg)


### Google側の設定
#### OAuth同意画面の設定
* [GCPのコンソール](https://console.developers.google.com)にアクセスして、
メニューから[APIとサービス]＞[OAuth同意画面]を選択します。
※既に作成済なら、この[OAuth同意画面の設定]の手順は不要です。読み飛ばして[認証情報の設定](#認証情報の設定)へと進んでください。
![OAuth同意画面.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/e3b65cdb-98b6-bfef-db7e-9039b285853a.jpeg)


* とりあえずここでは「外部」を選択して作成します。
![OAUth同意画面_内部_外部.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/ca3e23b7-2571-17ef-cad9-562f661cc669.jpeg)

* とりあえず必須項目だけ埋めてしまいます。その他の項目は必要に応じて設定してください。
設定したら[保存して次へ]をクリック。
![必須項目1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/d87556b2-3c85-79f0-f64d-b7cc10d274c4.jpeg)![必須項目2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/6374957b-b700-7e3c-6c7e-8ecf24a05e3c.jpeg)

* [スコープを追加または削除]をクリックして、選択肢から「opneid」を追加します。
設定したら[保存して次へ]をクリック。
![スコープの追加.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/a3fb0a22-849d-39e5-6906-fc671bdcf128.jpeg)

* 「③テストユーザ」と「④概要」については特に設定せず、そのまま[保存して次へ]をクリック。
![テストユーザ_概要.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/8bb364b3-2592-c694-48d5-e9bb269c0b6d.jpeg)


#### 認証情報の設定
* [GCPのコンソール](https://console.developers.google.com)にアクセスして、
メニューから[APIとサービス]＞[認証情報]を選択します。
※[OAuth同意画面の設定](#oauth同意画面の設定)が未設定の場合は先にそちらを設定してください。
![GCP認証情報.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/025224d3-4918-dea0-077e-c76887f9e5ec.png)

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

![OAuthクライアントID.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/202eb46c-1716-c728-8cdd-22e9023931db.png)

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

![webアプリケーションを選択.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/1f477aaf-93b2-c2d4-47aa-0d8a60713e60.png)

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

![OAuthクライアントIDの作成.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/b8486750-b21e-7b3a-d68a-5287faf681b0.jpeg)

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

![クライアントIDとクライアントシークレット.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/260159d0-c68e-9524-be92-46c9e59e937d.png)

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

![認証情報が作成できた.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/ae3970e1-2dab-1f0c-3899-4a06a7b29b7b.jpeg)

## 動かしてみる

1. XAMPPを起動する(既に起動している場合は不要)
1. [https://localhost/social-login/google/index.php](https://localhost/social-login/google/index.php)にウェブブラウザでアクセスする。
![ログイン画面.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/8fdba9ee-097f-7297-d6f8-db1668b9f9de.jpeg)
1. [Sign in with Google]のアイコンをクリックする。
1. Googleの認証画面に遷移するので、認証する。
![ログイン認証画面.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/7a8e7349-29fb-19a0-8252-33c50e90360a.jpeg)
1. 認証が成功すると[https://localhost/social-login/google/redirect.php](https://localhost/social-login/google/redirect.php)にリダイレクトされる。このページではユーザ情報などを表示しています。
![ユーザ情報.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689761/fee7db06-ea58-83fb-1084-72f5c620bad2.jpeg)

### 以上