[NativePHP]Google認証をfirebase無しで実装する

こんにちは。muchiです。

今回はフレームワークを使わない、素のphpで、更にfirebaseも使わずにGoogle認証を実装してみようと思います。

OAuth2.0ではなく、OpenID Connectです。IDトークンを使った認証になります。
(このあたりの定義は解説しないので、内容を見て判断してください。)

ただGoogleにおいてはOAuth2.0APIでもOpenID Connectに準拠しているとのこと。

・参考
https://developers.google.com/identity/protocols/oauth2/openid-connect

GCPにて準備

まずはGCP(google cloud platform)にアクセスして準備を行います。

左上の六角形のところをクリックし、プロジェクトを追加します。

作成したプログラムを選択し、
[APIとサービス]-[認証情報] と進みます。
この時点で「必ず、アプリケーションに関する情報を使用して OAuth 同意画面を構成してください。」と表示されている場合は先にそちらの設定を行います。

どうも外部に公開するためには審査が必要なようですね。
次の画面ではwebアプリに関する情報を求められるので、とりあえず最小限の内容を入力して次に。

スコープを選択する画面では、取得する情報を選択することができます。Google認証を実装するにあたって特に必要な情報はないです。ただ今回は練習も兼ねて、ユーザ情報とメールアドレスも取得してみます。

余談ですが、google playもしくはapp storeに公開するアプリで外部認証を使う場合、ユーザ名等の情報は各サービスから取得したものを初期値にセットしないと審査ではじかれてしまうので注意してください!

次の画面ではテストに参加するGoogleアカウントを指定します。〜〜@gmail.comの形で入力してください。
ここまで入力できたら認証情報を作成するための事前準備は完了です。

次に [認証情報の作成]-[OAuthクライアントID] を選択。

すると情報を入力する画面へ。

[名前]に入力する値はユーザには表示されず、管理用として使用されます。

承認済みのJavaScriptとリダイレクト先ですが、ローカル環境にてテストする場合はIPだとエラーが出るのでlocalhostにしなければならないと情報がありました。

次に進むと認証情報が表示されており、jsonファイルがダウンロードできるので保存しておいてください。このjsonファイルには各種情報が含まれているのでwebアプリ側でこれを読めば良いということなのですが、別途プログラム側で設定情報を定数等で管理する場合は、わざわざjsonを使う必要はありません。

PHP側で処理を実装する

続いてphp側の処理を記述します。ログイン用のphpとコールバック先のphpを用意してください。

ログイン用のphpでは各パラメータをセットしたリクエストを送信します。

session_start();
session_regenerate_id(true);

$stateHash = hash('sha256', 'rundomstring');
$nonceHash = hash('sha256', 'rundomstring2');

$_SESSION['google_state'] = $stateHash;
$_SESSION['google_nonce'] = $nonceHash;

$authUrl = 'https://accounts.google.com/o/oauth2/auth';
$params = array(
  'client_id' => '~.apps.googleusercontent.com',
  'redirect_uri' => 'http://localhost:8888/~',
  'scope' => 'profile email',
  'response_type' => 'code',
  'access_type' => 'offline',
  'state' => $stateHash, //CSRF対策。SESSIONにも保存しておき、コールバック先で同じか判定。
  'nonce' => $nonceHash //number of once. IDトークンのレスポンスと比較後、削除。
);
header('Location: https://accounts.google.com/o/oauth2/auth?' . http_build_query($params));

今回はセキュリティ対策も行うために、stateとnonceも設定しています。詳しい解説は省きますが、どちらもログイン前のページで発行したハッシュ値とログイン後(コールバック先)のリクエスト結果を比較することで、攻撃を防ぐことができます。必ずランダムな値をセットしておきましょう。

続いてCallback側で認証結果を取得するところまで。

  session_start();
  session_regenerate_id(true);

  //state検証
  $state = $_GET['state'];
  if($state != $_SESSION['google_state'])
  {
    echo '認証エラー';
    exit;
  }
  else
    unset($_SESSION['google_state']);

  $params = array(
    'code' => $_GET['code'],
    'grant_type' => 'authorization_code',
    'redirect_uri' => 'http://localhost:8888/~',
    'client_id' => '~.apps.googleusercontent.com',
    'client_secret' => 'クライアントシークレットをコピペ',
  );

  $options = array('http' => array(
    'method' => 'POST',
    'header' => array('Content-Type: application/x-www-form-urlencoded'),
    'content' => http_build_query($params)
  ));
  $resFromToken = file_get_contents('https://accounts.google.com/o/oauth2/token', false, stream_context_create($options));
  $token = json_decode($resFromToken, true);
  if (isset($token['error'])) {
      echo '認証エラー';
      exit;
  }
  $jwt = explode('.',$token['id_token']);
  $payload = json_decode(base64_decode($jwt[1]), true);
  //nonce検証
  $nonce = $payload['nonce'];
  if($nonce != $_SESSION['google_nonce'])
  {
    echo '認証エラー';
    exit;
  }
  else
    unset($_SESSION['google_nonce']);

  $accessToken = $token['access_token'];
  $params = array('access_token' => $accessToken);
  $resFromUserInfo = file_get_contents('https://www.googleapis.com/oauth2/v1/userinfo?' . http_build_query($params));
  $userInfo = json_decode($resFromUserInfo, true);
  var_dump($userInfo);
  //それぞれ取得する場合
  echo $userInfo["email"];
  echo $userInfo["name"];

callbackでおこなっている処理の流れとしては、

1.callback前と後のstate比較
2.callback時に付加されているcodeを使ってGoogleAPIへのアクセストークン(JWT)取得
3.JWTのペイロード部分のnonceをcallback前と後で比較
4.アクセストークンを付加してgoogleにユーザ情報をリクエスト

です。stateとnonceがわからない方は調べてみてください。
どちらも不正な攻撃を防ぐためのものです。googleも必須もしくは推奨としているので、必ず実装しましょう。
ちなみにここで取得できるIDは固定値のようです。

まとめ

ログインやAuth辺りはフレームワークやSDKを使えば何となく実装することは可能ですが、その場合実は最低限のセキュリティしか担保されておらず、結果的にリスキーな状態だったりします。このコードをコピペすれば動いてしまいますが、一度自分で実装しようとするととても勉強になるので、調べながら挑戦してみてください。