OAuthをcurlとphpでやってみる
去年10月から ソーシャルPLUS にフロントエンドエンジニアとして業務委託で参画しているほっしゃん(https://github.com/hotsum92)です!!!
あまりログイン関連の技術については、触ったことがなかったのですが、ソーシャルPLUSに参画してから、OIDCを勉強させていただきました。今回は、その基本となるOAuth演舞を舞ってやろうとおもいます。
勉強はしてみたけど、よくわからないみたいな状況をぬけだすために、とりあえず動くものを作ってみることにします。なるべくシンプルな環境で動作確認をしていきたいので、curlとphpで、OAuthの動きを真似してみようとおもいます。今回、phpのビルトインウェブサーバーで認可サーバを雑に実装しますが、keycloak
などを利用するとより正確な動きを見ることができます。OAuthのフロー図から実際に動くものを実装する足がかりになるような記事をめざします。
追ってみるOAuthのシナリオは、認可コードフロー。あるウェブアプリケーションがユーザーの情報を必要として、ユーザーはその情報にアクセスする権限を付与するという状況をやってみようと思います。
リソースが必要となるページにアクセス
図の「ブラウザ」役は、curl。残りの「サーバー」と「リソース」役をphpのビルトインウェブサーバーで実装していきます。今回の実装では、「リソース」は認可サーバーとしての役割も担っています。
まず、「サーバー」を用意します。server
フォルダーにindex.php
を用意して、curlでアクセスしてみます。
server/index.php
<?php
echo "Web Application\n";
「サーバー」の起動
$ cd ./server
$ php -S localhost:8080
「サーバー」にアクセス
$ curl 'http://127.0.0.1:8080'
Web Application
権限の付与を行うために、リソースにリダイレクト
「サーバー」がユーザーの情報を取得するため、ユーザーに「リソース」にアクセスしてもらい、権限の付与を行ってもらいます。
server/index.php
を編集して、「サーバー」にアクセスされたらパラメータとともに、「リソース」にリダイレクトされるようにします。また、redirect_uri
に認可完了後に戻ってくるリダイレクト先を指定します。
server/index.php
$url = "http://127.0.0.1:9000";
$url .= "?response_type=code";
$url .= "&client_id=server";
$url .= "&redirect_uri=" . urlencode("http://127.0.0.1:8080/callback");
$url .= "&state=RANDOM_STATE";
header("HTTP/1.1 302 Found");
header("Location: $url");
resource
フォルダーにindex.php
を用意し、「リソース」側を用意します。
httpのリクエストの動きだけを追ってみたかったので、ユーザーに権限の付与の可否を尋ねるプロンプトだけを表示するようにしていますが、本来は「サーバー」からのパラメータの検証を行います。
resource/index.php
<?php
echo "Yes or No?\n";
「リソース」の起動
$ cd ./resource
$ php -S localhost:9000
curlでアクセスしてみて動作確認してみます。リダイレクトが発生するので、-L
でリダイレクトに対応します。ここでは、「Yes or No?」と表示していますが、実際はブラウザを使用しているユーザーが「はい」のボタンをクリックするような認可を行う場面になります。
$ curl -L 'http://127.0.0.1:8080'
Yes or No?
ユーザーが権限を許可し、認可コードを送信
ユーザーが権限を許可した後に、認可サーバーも担っている「リソース」が「サーバー」に認可コードを送信します。
resource/approve/index.php
に、ユーザーの許諾の処理を実装します。approve
が実行されると、認可コード(code=RANDOM_CODE
)を「サーバー」側に送信します。「サーバー」に直接認可コードを送信しているのではなく、URLに認可コードを入れてブラウザーを介して送信しています。httpsが前提でOAuthが設計されていることがわかります。"http://127.0.0.1:8080/callback"
の値は、ここではハードコードされていますが、redirect_uri
に設定されている値です。
resource/approve/index.php
<?php
$url = "http://127.0.0.1:8080/callback";
$url .= "?code=RANDOM_CODE";
$url .= "&state=RANDOM_STATE";
header("HTTP/1.1 302 Found");
header("Location: $url");
server/callback/index.php
に認可コードが送信されていることを確認する実装を追加します。
server/callback/index.php
<?php
$code = $_GET['code'];
echo "code: $code\n";
curl でユーザーの認可を行ってみると、「サーバー」側に、認可コードが送信されていることがわかります。
ユーザーの認可
$ curl -L http://127.0.0.1:9000/approve
code: RANDOM_CODE
認可コードで、アクセストークンを取得
ユーザーの認可によって得られた認可コードで、アクセストーコンを取得します。
「リソース」から戻って来た際の「サーバー」のcallback
にアクセストークンの取得処理を実装します。「サーバー」は、「リソース」と直接通信する際に、Basic認証をヘッダーに、ボディに認可コードを含めてからPOST
メソッドで、アクセストークンの取得処理を行っています。ここでの処理は、「サーバー」と「リソース」だけで行われ、ブラウザーを介すことはありません。そのため、アクセストークンはブラウザーが知ることはありませんが、動作確認のため最後にアクセストークンを出力しています。
server/callback/index.php
<?php
$code = $_GET['code'];
$url = "http://127.0.0.1:9000/token";
$authorization = "Basic " . base64_encode("server:server-secret");
$data = array(
"grant_type" => "authorization_code",
"code" => $code,
"redirect_uri" => "http://127.0.0.1:8080/callback",
);
$options = array(
"http" => array(
"header" => "Authorization: $authorization\r\nContent-type: application/x-www-form-urlencoded\r\n",
"method" => "POST",
"content" => http_build_query($data),
),
);
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
$access_token = json_decode($result)->access_token;
echo "access_token: $access_token\n";
「リソース」にアクセストークンを送信する処理を実装します。アクセストークンをJSONオブジェクトとして返します。Basic認証や、認可コードの検証など様々な検証が本来は含まれますが、ログに出力するのみにとどめています。
resource/token/index.php
<?php
$authorization = $_SERVER["HTTP_AUTHORIZATION"];
$code = $_POST["code"];
error_log(print_r("authorization: $authorization", true));
error_log(print_r("code: $code", true));
echo json_encode(array(
"access_token" => "access-token",
"token_type" => "bearer",
"refresh_token" => "refresh-token"
));
動作確認してみると、「サーバー」でアクセストークンを取得できていることがわかります。
ユーザーの認可
$ curl -L http://127.0.0.1:9000/approve
access_token: access-token
アクセストークンを使って、リソースにアクセス
「サーバー」のcallback
に取得したアクセストークンでリソースにアクセスする実装を追加します。
アクセストークンをヘッダーに含めて、ユーザーの情報を取得しています。
server/callback/index.php
<?php
$code = $_GET['code'];
$url = "http://127.0.0.1:9000/token";
$authorization = "Basic " . base64_encode("server:server-secret");
$data = array(
"grant_type" => "authorization_code",
"code" => $code,
"redirect_uri" => "http://127.0.0.1:8080/callback",
);
$options = array(
"http" => array(
"header" => "Content-type: application/x-www-form-urlencoded\r\n",
"header" => "Authorization: $authorization\r\n",
"method" => "POST",
"content" => http_build_query($data),
),
);
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
$access_token = json_decode($result)->access_token;
$url = "http://127.0.0.1:9000/resource";
$context = stream_context_create([
"http" => [
"method" => "GET",
"header" => "Authorization: Bearer $access_token\r\nContent-type: application/x-www-form-urlencoded\r\n"
],
]);
$result = file_get_contents($url, false, $context);
echo $result;
「リソース」にユーザーの情報を返す処理を実装します。ここでは、提示されたアクセストークンが有効かどうか検証する処理が本来ふくまれますが、ログに出力するのみにしています。
resource/resource/index.php
<?php
$authHeader = $_SERVER['HTTP_AUTHORIZATION'];
error_log(print_r("Authorization: " . $authHeader, true));
echo "User information!!!\n";
最後に、ユーザーの認可を再度実行すると、「リソース」から認可コードを取得し、アクセストークンと交換して、「サーバー」がユーザーの情報を取得できていることがわかります。
ユーザーの認可
$ curl -L http://127.0.0.1:9000/approve
User information!!!
Discussion