1.はじめに
GoogleHome経由でサーバーに何か働きかけをする手段としてIFTTTを通してウェブフックを呼び出す方法などありますが、外部サービスということやつい先日もAPIの変更とかでそれまで使っていたアプレットが使えなくなって再定義が必要になる、といったこともありました。できればサーバーそのものが家電の一種として登録できたらいいのにと思わなくもありません。リモートスピーカーとかサウンドバーのようなデバイスとして見せかけられたらGoogleHomeの使い勝手も少し良くなるような気がします。
ただ、Google Assistantのドキュメントなど読んでいると、何が無くともまずは OAuth2.0サーバー を用意することが必要とのこと。
言語はいろいろありますが、個人的にイージーに使えるということもあり、PHPで探してみると、OAuth 2.0 Server PHPというものが見つかりました。本記事作成時点でv1.10が公開されてから5年経過していますが、5カ月前にはPHP8.1向けのエラー修正がされているようです。
このライブラリにはステップ・バイ・ステップで構築するスタートアップのガイダンス(Step-By-Step Walkthrough)もあり、とっつき易そうなので使うことにしました。
大まかな流れとしては次のようになります。
- ライブラリを展開する
- リソースDBを整備する
- トークン制御ページを作る
- リソース制御ページを作る
- 認可ページを作る
"Step-By-Step Walkthrough"は上記の流れに従い、適時動作確認を挟みながら順を追って構築していく内容になっています。本記事はOAuth2.0 Server PHPの"Step-By-Step Walkthrough"の簡訳のような内容となります。
なお、OAuth 2.0 Server PHPを導入した環境ですが、概要としては以下のようになります。
- Linux 5.19.15-201.fc36
- Apache 2.4.54
- PHP 8.1.10
- PostgreSQL(libpq) 14.1
- OAuth2.0 Server PHP 1.10
以下の章で詳細に入りますが、実装コードのうち行が多いものについては省いています。コードの詳細については(Step-By-Step Walkthrough)をご参照ください。
2.OAuth2.0サーバーを実装する
2.1.ライブラリを展開する
まず、ライブラリ用の管理ディレクトリを作成して、gitからクローンを取りました。
git clone https://github.com/bshaffer/oauth2-server-php
oauth2-server-php/srcディレクトリ以下にphpのライブラリがあるので、これをWebサーバから参照できる(googleAssistantのSDKで言うところの)フルフィルメントAPIリソースを置くディレクトリから参照できるようにしました。
一応利用可能になっていることの確認として、"An OAuth2 Server Library for PHP"の"Installation"にあるサンプルコードを使ってサンプルページ(sample.php)を作りました。ただ、サンプルコード中のrequire_onceについては自環境の実装に合わせたパスに切りなおします。
require_once('/path/to/oauth2-server-php/src/OAuth2/Autoloader.php');
OAuth2\Autoloader::register();
php -f sample.php で読み込ませてエラーが出ないことを確認しました。
2.2.リソースDBを整備する
ライブラリはPDO関数でDBをラッピングしているのでDBの種別は接続文字列以外は区別しません。手元の環境ではPostgreSQLがあるので、それを使いました。DB整備の流れとしては次のようにしています。
- OAuth2.0 Server PHP用のロールとDBを作成
- OAuth2.0 Server PHPが使うテーブルを作成
- OAuth2.0 Server PHPライブラリとDBの接続を確認
2.2.1.ロールとDBを作成する
psqlの管理ユーザーで接続して専用ロール(ユーザ)を作成しました。
ユーザ名は(仮に)'phpoa2'とします。
$ psql -U postgres
psql (14.3)
"help"でヘルプを表示します。
postgres=# CREATE USER phpoa2
PASSWORD '********'
CREATEDB;
CREATE ROLE
postgres=# quit
$
作成したユーザーで接続し直し、OAuth2.0用データベースを作成しました。
$ psql -U phpoa2 -d postgresql
psql (14.3)
"help"でヘルプを表示します。
phpoa2=> CREATE DATABASE phpoa2
encoding 'UTF8'
lc_collate 'ja_JP.UTF-8'
lc_ctype 'ja_JP.UTF-8';
phpoa2=> quit
作成したユーザーでpsqlにデフォルトデータベースで接続できることを確認します。
$ psql -U phpoa2
psql (14.3)
"help"でヘルプを表示します。
phpoa2=> quit
$
2.2.2.テーブルを作成する
Setp-By-Step Walkthroughにある"Define your Schema"にあるSQLをそのままコピーペーストで実行します。以下のテーブルが作成されます。
- oauth_clients
- oauth_access_tokens
- oauth_authorization_codes
- oauth_refresh_tokens
- oauth_users
- oauth_scopes
- oauth_jwt
\dt を実行して上記のテーブルだけが表示され、所有者がphpoa2であることを確認します。
$ psql -U phpoa2
psql (14.3)
"help"でヘルプを表示します。
phpoa2=> \dt
リレーション一覧
スキーマ | 名前 | タイプ | 所有者
----------+---------------------------+----------+--------
public | oauth_access_tokens | テーブル | phpoa2
public | oauth_authorization_codes | テーブル | phpoa2
public | oauth_clients | テーブル | phpoa2
public | oauth_jwt | テーブル | phpoa2
public | oauth_refresh_tokens | テーブル | phpoa2
public | oauth_scopes | テーブル | phpoa2
public | oauth_users | テーブル | phpoa2
(7 行)
phpoa2=> quit
2.2.3.ライブラリとリソースDBを接続する
"Bootstrap your OAuth2 Server"にあるサンプルコード(server.php)を使い、サーバーインスタンスの接続ページを作成します。src/OAuth2以下にも同名のファイルがありますが、このファイルとは別物です。
サンプルコードの先頭3行にある $dsn, $username, $password
を実装環境に合わせて修正しました。
$dsn = 'mysql:dbname=my_oauth2_db;host=localhost';
$username = 'root';
$password = '';
今回のpostgresql接続でphpoa2ユーザを使う場合だと次のようになります。
$dsn = 'pgsql:dbname=phpoa2 host=localhost';
$username = 'phpoa2';
$password = '********';
server.phpの修正が終わったら php -f server.php
を実行してエラーが出ないことを確認しました。
2.3.トークン制御ページを作る
"Create a Token Controller"にあるコードを使い、トークン制御ページ(token.php)を作ります。このページはOAuth2.0トークンをクライアントに戻すURIとなります。
// include our OAuth2 Server object
require_once __DIR__.'/server.php';
// Handle a request for an OAuth2.0 Access Token and send the response to the client
$server->handleTokenRequest(OAuth2\Request::createFromGlobals())->send();
このページの動作確認のため、リソースDBにテスト用のダミークライアントを登録します。
登録にはSQLを使います。
psql -U phpoa2
psql (14.3)
"help"でヘルプを表示します。
phpoa2=> INSERT INTO oauth_clients (client_id, client_secret, redirect_uri)
VALUES ('testclient', 'testpass', 'http://fake/');
client_idはダミークライアントのID、client_secretはダミークライアントの秘密鍵(的なところでしょうか。単に非公開の文字列)になります。
redirect_uriに設定しているhttp://fake/
は本来は実際のクライアントへのリダイレクトURIになりますが、ここではあくまでも動作確認で使われないためダミーになります。
INSERTでエラーがなければ応答確認をします。
curl -u testclient:testpass https://your.domain/token.php -d 'grant_type=client_credentials'
次のような感じのjson文字列が出力されれば大丈夫です。
{"access_token":"cd042ade5fed1cdc7b0a77784e47728527a1fa7f","expires_in":3600,"token_type":"Bearer","scope":null}
2.4.リソース制御ページを作る
"Create a Resource Controller"にあるコードを使い、リソース制御ページ(resource.php)を作成します。
// include our OAuth2 Server object
require_once __DIR__.'/server.php';
// Handle a request to a resource and authenticate the access token
if (!$server->verifyResourceRequest(OAuth2\Request::createFromGlobals())) {
$server->getResponse()->send();
die;
}
echo json_encode(array('success' => true, 'message' => 'You accessed my APIs!'));
リソース制御ページの動作確認をします。引数に使うaccess_tokenの値は2.3.動作確認で得られたaccess_tokenの値を使います(‘YOUR_TOKEN'を置き換えます)。
$ curl https://your.domain/resource.php -d 'access_token=YOUR_TOKEN'
{"success":true,"message":"You accessed my APIs!"}
2.5.認可ページを作成する
"Create an Authorize Controller"にあるコードを使い、認可ページ(authorize.php)を作成します。認可ページはtoken.phpで使ったaccess_tokenを返します。
認可ページを動作確認する。Webブラウザで以下のURLにアクセスする。('your.domain`やauthorize.phpのパスはそれぞれの環境に合わせます)
https://your.domain/authorize.php?response_type=code&client_id=testclient&state=xyz
ブラウザには'yes' 'no'ボタンが並ぶ画面が表示されるので'Yes'ボタンをクリックすると認証コード(Authorization Code:)が表示されます。この認証コードを使ってtoken.phpを2.3.の要領でアクセスします。ただし、認証コード(authorization_code)情報を-dの引数で与えます。この認可ページで得られる認証コードの寿命は30秒なので、メモ帳などで認証コードを埋め込んで使えるテンプレートを作ってコピー&ペーストして使えるようにしておくと良いでしょう。以下の例では認証コードをYOUR_CODEとしています。
curl -u testclient:testpass https://your.domain/token.php -d 'grant_type=authorization_code&code=YOUR_CODE'
2.6.ユーザIDと紐づけたい場合
前項の2.5.まで進めた時点で、例えばGoogleAssistantからの認可要求には対応できるようになります。ただ、OAuth2.0 Server PHPの"Step-By-Step Walkthrough"ではユーザIDと認可コードを紐づけたい場合、として以下のような実装が紹介されています。実装はauthorization.php
の3か所を変更します。
2.5.で作成したauthorize.phpの入力フォームにユーザIDの入力欄を追加します。
----------------------変更①----------------------
<form method="post">
<label>Do You Authorize TestClient?</label><br />
+ <input type="text" name="uid" /><br />
<input type="submit" name="authorized" value="yes">
<input type="submit" name="authorized" value="no">
</form>');
認証リクエストのコードにユーザIDを追加します。
----------------------変更②----------------------
-$server->handleAuthorizeRequest($request, $response, $is_authorized);
+$server->handleAuthorizeRequest($request, $response, $is_authorized, $_POST['uid']);
resource.phpを、コード確認時にユーザIDを取得するように変更する。
----------------------変更③----------------------
-echo json_encode(array('success' => true, 'message' => 'You accessed my APIs!'));
+echo json_encode(array('success' => true, 'message' => 'You accessed my APIs!'))."\n";
+$token = $server->getAccessTokenData(OAuth2\Request::createFromGlobals());
+echo "User ID associated with this token is {$token['user_id']}"."\n";
ユーザIDと紐づけされたことの動作確認は次のようになります。
Webブラウザで以下のURLにアクセスして認証コードを取ります。
https://your.domain/authorize.php?response_type=code&client_id=testclient&state=xyz
トークン制御ページに取得した認証コード(YOUR_CODEを置き換えます)を使ってアクセスします。
curl -u testclient:testpass https://your.domain/token.php -d 'grant_type=authorization_code&code=YOUR_CODE'
次にリソースコントローラを呼び出します。access_tokenはトークン制御ページから返ってきたaccess_token(ACCESS_CODEを置き換えます)を使います。
curl https://www.freiheitstrom.net/~saitou/oa2/resource.php -d 'access_token=ACCESS_CODE'
access_tokenとユーザIDとの紐づきはリソースDBのoauth_access_tokensに格納されています。
3.おわりに
これでOauth2.0サーバーを動かすことができました。authorization.phpは外部で目につくページとなるので、もう少し色付けした方が良さそうです。特に、そもそも個人的な目的としているGoogleAssistantとの連携ではスマホからの操作になりますが、authorization.phpの表示はスマホ向けではないので、だいぶ操作しずらいページになります(使えなくはない)。
GoogleAssistantとの連携については稿を改めてまとめたいと思っています。