LoginSignup
87

More than 3 years have passed since last update.

Laravel による OAuth 2.0 と OpenID Connect の実装(Authlete)

Last updated at Posted at 2018-05-22

はじめに

Laravel ユーザーに朗報です!

authlete/authlete-laravel』ライブラリをリリースしました! このライブラリにより、OpenID Certification 取得済みの認可エンジン『Authlete』をバックエンドに用いた本格的な認可サーバー・OpenID プロバイダーの開発が、Laravel で可能となりました!

この記事では、PHP 用のフレームワークの一つである Laravel を用いて、OAuth 2.0 と OpenID Connect に対応する認可サーバー(兼 OpenID プロバイダー)とリソースサーバーを実装する方法を紹介します。実装には、PHP 共通の authlete/authlete ライブラリと Laravel 専用の authlete/authlete-laravel ライブラリを使用します。

なお、実例として Laravel を用いますが、認可サーバー・リソースサーバーを実装する際に必要となる一般的な基礎知識についても必要に応じて説明しますので、参考にしていただければと思います。

断り書き

PHP に不慣れで、Laravel に関しては完全に初見、という理由もありますが、開発ツールによる支援の差が大きく、他のプログラミング言語であればコーディング中に確実に気付くであろう単純な不具合を、実際に動かしてみるまで気付けなかったということが何度もありました。おそらく現在も何かしら信じられない不具合が隠れていると思います。何か気付かれましたら、GitHub の authlete/authlete-php もしくは authlete/authlete-php-laravel にて Issue を登録してくださるか、support@authlete.com までご連絡ください。よろしくお願いします。

0. せっかちな人向け

Laravel プロジェクト

$ laravel new authorization-server
$ cd authorization-server

Authlete

$ composer require authlete/authlete-laravel
$ vi config/app.php
    # Laravel 5.5 より古い場合、手作業で providers に下記を追加。
    # Authlete\Laravel\Provider\AuthleteServiceProvider::class
$ php artisan authlete:authorization-server
$ vi config/authlete.php
    # Authlete から発行された API キーと API シークレットを、それぞれ service_api_key と service_api_secret に記述。

Laravel 認証

$ vi .env
    # DB_CONNECTION の値を sqlite に変更。
    # 他の DB_* で始まる変数群をコメントアウト。
$ touch database/database.sqlite
$ composer require laravel/ui
$ php artisan ui vue --auth
$ npm install && npm run dev
$ php artisan migrate
$ php artisan serve
    # http://127.0.0.1:8000/register でユーザー登録。

インプリシットフロー

インプリシットフローによるアクセストークン要求
http://127.0.0.1:8000/authorization?response_type=token&client_id={クライアントID}
インプリシットフローによる ID トークン要求
http://127.0.0.1:8000/authorization?response_type=id_token&client_id={クライアントID}&redirect_uri=https://api.authlete.com/api/mock/redirection/{サービスAPIキー}&scope=openid+email+profile&nonce={ノンス}

認可コードフロー

認可リクエスト

http://127.0.0.1:8000/authorization?response_type=code&client_id={クライアントID}

トークンリクエスト

$ curl http://127.0.0.1:8000/api/token \
    -d grant_type=authorization_code \
    -d code={認可コード} \
    -d client_id={クライアントID}

1. API 公開のために何をすべきか

「Facebook が Web API を提供しているように自社のサービスでも Web API を提供したい」という考えに至った場合、用意しなければならないものは、次の二つのサーバーです。

サーバー 役割
1 認可サーバー アクセストークンを発行する
2 リソースサーバー Web API を提供する

認可サーバーとリソースサーバーの概念は、『一番分かりやすい OAuth の説明』という記事で説明しています。しかしながら、初学者向けの説明になっているため、当該記事内の情報だけでは具体的な実装イメージが湧きません。そこで、ここではもう少し技術的に掘り下げてみます。

1.1. 認可サーバーは何をすべきか

認可サーバーが提供すべきなのは、次の二つのエンドポイントです。

エンドポイント
1 認可エンドポイント
2 トークンエンドポイント

ユースケースを限定すれば、どちらか一方のエンドポイントだけでもかまいませんが、最もよく使われる『認可コードフロー』と呼ばれるアクセストークン発行フローをサポートする場合、両方のエンドポイントを提供する必要があります。アクセストークン発行フローと使用されるエンドポイントの関係については、『OAuth 2.0 全フローの図解と動画』および『OpenID Connect 全フロー解説』をご参照ください。

クライアントアプリケーションは、認可エンドポイントとトークンエンドポイントの両方、もしくはどちらか一方と HTTP 通信をおこない、アクセストークンを取得します。

client-application-and-authorization-server.png

この、認可サーバーとクライアントアプリケーションのやりとりの詳細を定義している仕様書が RFC 6749(The OAuth 2.0 Authorization Framework)です。そのため、エンジニアは RFC 6749 を読んで認可サーバーを実装することになります。加えて、RFC 7636(Proof Key for Code Exchange by OAuth Public Clients)や OpenID Connect Core 1.0 などの他の仕様書も必要に応じて参照します。

なお、認可サーバーは、認可エンドポイントとトークンエンドポイント以外のエンドポイントを提供することもあります。例えば次のようなエンドポイントがあります。

エンドポイント 関連仕様書 用途
3 イントロスペクションエンドポイント RFC 7662 アクセストークンの情報を取得する
4 リボケーションエンドポイント RFC 7009 アクセストークンを取り消す
5 ディスカバリーエンドポイント OIDC Discovery 1.0 サーバーの設定情報を取得する
6 JWK Set ドキュメントエンドポイント RFC 7517 サーバーの JWK Set ドキュメントを取得する

1.2. リソースサーバーは何をすべきか

リソースサーバーは Web API を提供します。

サービスの内容に応じて自由な Web API を定義・公開してかまいません。どのような Web API を定義・公開すべきかについては、RFC 6749 では決められていません。代わりに、Web API の守り方が述べられています。基本的なアイディアは次の通りです。

  1. 「Web API にアクセスする権利」を表すアクセストークンを生成する。
  2. そのアクセストークンをあらかじめクライアントアプリケーションに渡しておく。
  3. クライアントアプリケーションは Web API にアクセスする際、アクセストークンを提示する。
  4. Web API は、提示されたアクセストークンが有効であれば求められた応答を返し、無効であればエラー応答を返す。

client-application-and-resource-server.png

Web API がアクセストークンを受け取る方法はいくらでも考案することができますが、特に理由がなければ、RFC 6750(The OAuth 2.0 Authorization Framework: Bearer Token Usage)で定められている標準的な方法に従うことをおすすめします。RFC 6750 の詳細については、『【第二弾】OAuth 2.0 + OpenID Connect のフルスクラッチ実装者が知見を語る』の「2.2. アクセストークンの受け取り方」で解説していますので、ご参照ください。

どのような Web API を定義・公開すべきかに関する言及が RFC 6749 内に無いことは先に述べたとおりですが、OpenID Connect の中心となる仕様書『OpenID Connect Core 1.0』では、 「5.3. UserInfo Endpoint」において、ユーザー情報エンドポイントという Web API を定義しています。

Web API 関連仕様書 用途
1 ユーザー情報エンドポイント OIDC Core 1.0 ユーザーに関する情報を取得する

2. Authlete

この記事では、authlete/authlete-laravel ライブラリを用いて認可サーバーとリソースサーバーを実装します。このライブラリは、正確には、このライブラリが使用している authlete/authlete ライブラリは、 Authlete(オースリート)というサービスと通信をおこないます。

Authlete は、OAuth 2.0 と OpenID Connect を実装するのに必要な機能を Web API として提供しているサービスです。Authlete を利用する認可サーバーとリソースサーバーは、その実装の中から Authlete サーバーと通信をおこないます。

client-application-servers-and-authlete.png

OAuth 2.0 と OpenID Connect のロジックは Authlete 側で実装されており、また、発行したアクセストークン等の永続記憶領域に保存しておくべきデータは Authlete 側のデータベースで管理されます。このため、Authlete を用いると、認可サーバーの実装作業はかなり楽になります。Authlete のアーキテクチャーの詳細については、次の記事をご参照ください。

2.1. Authlete API にアクセスするための API キー

Authlete と通信をするためには、Authlete から発行される、サービス API キーとサービス API シークレットの組が必要です。後ほど API キーと API シークレットを使用するので、まだお持ちでなければ、『Spring + OAuth 2.0 + OpenID Connect』の「3.1. Authlete API にアクセスするための API キー」を参考に、取得してください。

2.2. クライアント ID

認可サーバーにリクエストを投げるためには、クライアント ID が必要です。後ほど必要となるので、『Spring + OAuth 2.0 + OpenID Connect』の「3.2. クライアント ID」を参考に、取得してください。

2.3. Authlete 社の紹介

(以降の技術説明に影響はないので、ここは読み飛ばしてもらってもかまいません)

Authlete 社は 2015 年 9 月に設立された会社で、2018 年 5 月現在、東京・大手町の FINOLAB とロンドン・カナリーワーフの Level39 にオフィスを構えています。

主力サービスの Authlete(社名と同じ)は、創業翌年には株式会社エムティーアイ(東証一部 9438)が運営する大規模ヘルスケアサービスの認可サーバーに採用されました。2017 年の FIBC(Financial Innovation Business Conference)において大賞受賞後(発表動画)は、NRI セキュアテクノロジーズ株式会社Uni-ID Libraインタビュー記事)や日本ユニシス株式会社Resonatex の認可処理エンジンとして、また、セブン銀行の認可サーバーに採用されるなど、採用実績を伸ばしています。

2017 年 5 月には、株式会社エムティーアイと 500 Startups Japan を引受先とする1回目の資金調達を発表。2018 年 2 月には、凸版印刷株式会社(東証一部 7911)、株式会社 NTT ドコモ・ベンチャーズ、株式会社エムティーアイを引受先とする2回目の資金調達を発表しています。

3. 認可サーバーの実装

前置きが長くなりましたが、認可サーバーの実装に入りましょう。

3.1. Laravel プロジェクト作成

$ laravel new authorization-server
$ cd authorization-server

3.2. authlete-laravel ライブラリ導入

$ composer require authlete/authlete-laravel

Laravel のバージョンが 5.5 よりも古い場合、自動ディスカバリー機能が働かないので、AuthleteServiceProvider を手作業でプロバイダーのリストに追加します。config/app.php ファイルを開き、providers のリストに下記のように追加してください。

'providers' => [
    // 他のサービスプロバイダー群

    Authlete\Laravel\Provider\AuthleteServiceProvider::class,
],

3.3. authlete:authorization-server コマンド

authlete-laravel ライブラリを導入することにより、Artisan 用のコマンド authlete:authorization-server が使えるようになるので、実行してください。

$ php artisan authlete:authorization-server

authlete:authorization-server コマンドにより次の処理がおこなわれます。

  1. 設定ファイル config/authlete.php を生成(既に存在する場合は上書きしない)
  2. routes/web.php にルーティング設定を追加
  3. routes/api.php にルーティング設定を追加
  4. app/Http/Controllers/Authlete にコントローラー群を生成
  5. resource/views/authlete/authorization.blade.php を生成
  6. public/css/authlete/authorization.css を生成

authlete:authorization-server コマンドの動作詳細については、AuthleteAuthorizationServerCommand.php を参照してください。

3.4. Authlete 設定ファイル

php artisan authlete:authorization-server により、設定ファイル config/authlete.php が生成されます。初期状態の内容は下記のようになっています。

<?php
return [
    'base_url'                 => 'https://api.authlete.com',
    'service_owner_api_key'    => '',
    'service_owner_api_secret' => '',
    'service_api_key'          => '',
    'service_api_secret'       => ''
];
?>

service_api_keyservice_api_secret の値が空になっているので、ここに、Authlete から発行されたサービス API キーとサービス API シークレットを設定してください。

3.5. ディスカバリーエンドポイント

認可エンドポイントの実装に取り掛かりたいところですが、まず、Authlete との疎通確認を兼ねて、ディスカバリーエンドポイントを実装してみます。

3.5.1. ディスカバリーエンドポイントとは

ディスカバリーエンドポイントとは、サーバーの設定情報を JSON 形式で返すエンドポイントで、OpenID Connect Discovery 1.0 の「4. Obtaining OpenID Provider Configuration Information」で詳細が定義されています。同仕様書の「4.2. OpenID Provider Configuration Response」には、応答 JSON の例が挙げられています。実例としては、Google 社の https://accounts.google.com/.well-known/openid-configuration などがあります。

3.5.2. ディスカバリーエンドポイントの実装

authlete/authlete-laravel ライブラリには、ディスカバリーエンドポイントの実装である DefaultConfigurationController クラスが含まれています。このクラスだけで実装が完結しており、追加作業は必要ありません。

DefaultConfigurationController を親クラスとする ConfigurationController クラスが、php artisan authlete:authorization-server により app/Http/Controllers/Authlete フォルダにコピーされているので、ConfigurationController をそのままディスカバリーエンドポイントの実装として使用できます。

routes/web.php ファイルを開き、末尾に次の行を追加してください。

Route::get('/.well-known/openid-configuration',
  '\App\Http\Controllers\Authlete\ConfigurationController');

・・・いや、上記の内容は php artisan authlete:authorization-server により既に追加済みなので、実はここでは何もやらなくてよいです。

3.5.3. ディスカバリーエンドポイントの動作確認

php artisan serve でビルトイン Web サーバーを起動しましょう。

$ php artisan serve
Laravel development server started: <http://127.0.0.1:8000>

ポート 8000 で Web サーバーが起動するので、Web ブラウザや curl コマンドなどでディスカバリーエンドポイントにアクセスしてみてください。

ディスカバリーエンドポイント
http://127.0.0.1:8000/.well-known/openid-configuration

次のような JSON が返ってくれば成功です。

discovery-endpoint-response.png

ディスカバリーエンドポイントのパスが .well-known/openid-configuration になっていますが、このパス名については OpenID Connect Discovery 1.0 の「4. Obtaining OpenID Provider Configuration Information」に説明があるので、詳細はそちらを参照してください。

3.6. 認可画面の構成

認可サーバーの役割は、クライアントアプリケーションに対してアクセストークンを発行することです。アクセストークンは、「あるユーザーが、あるクライアントアプリケーションに対して、あることを許可した」ことを示すものです。

クライアントアプリケーションが認可サーバーに対してアクセストークンの発行を要求すると、認可サーバーは、アクセストークン発行に先立ち、アクセストークンを発行してよいかどうかをユーザーに確認します。確認画面は認可画面とも呼ばれ、おおむね次のような構成になっています。

authorization-page.png

認可サーバーの実装は、このような認可画面を生成し、ユーザーに提示する必要があります。

3.7. ユーザー認証

前出の認可画面内でログイン ID とパスワードを入力する箇所は、ユーザーを特定するための入力項目です。いわゆる「ユーザー認証」のための入力項目です。

上記の例ではログイン ID とパスワードでユーザー認証をおこなっていますが、ユーザーが特定できるのであれば、ユーザー認証の方法は何でもかまいません(指紋認証・ハードウェアトークン・乱数表・etc)。また、ユーザー認証の確度を上げるため、複数の認証方法を組み合わせてもかまいません(いわゆる多要素認証)。

OAuth 2.0 の仕様は、その名称(The OAuth 2.0 Authorization Framework)が示す通り、認可(Authorization)のための仕様であり、認証(Authentication)のための仕様ではありません。RFC 6749 の「3.1. Authorization Endpoint」には、ユーザー認証の方法は「この仕様の範囲外である」と書いてあります。

The authorization endpoint is used to interact with the resource owner and obtain an authorization grant. The authorization server MUST first verify the identity of the resource owner. The way in which the authorization server authenticates the resource owner (e.g., username and password login, session cookies) is beyond the scope of this specification.

ユーザー認証の方法は仕様では定められていないものの、しかしながら、認可処理内の一つのステップとしてユーザー認証が含まれているため、認可サーバーは何らかのユーザー認証の仕組みを実装する必要があります。

3.8. Laravel 認証

ユーザー認証の仕組みの作り込みは本記事の主題から離れてしまうので、ここでは、『Laravel 認証』を使って簡易的に実装してみます。

3.8.1. データベース設定

まず、ユーザー情報を保存するデーターベースの設定をしておきます。デフォルトは MySQL になっていますが、簡易実装ということで SQLite を使うことにします。

.env ファイルを開くと、DB_ で始まる項目が次のように列挙されていると思います。

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret

DB_CONNECTION の値を mysql から sqlite に変更し、他の DB_ 項目群はコメントアウトします。

DB_CONNECTION=sqlite
#DB_HOST=127.0.0.1
#DB_PORT=3306
#DB_DATABASE=homestead
#DB_USERNAME=homestead
#DB_PASSWORD=secret

そして、空のデータベースファイルを作成しておきます。

$ touch database/database.sqlite

3.8.2. make:auth コマンド

make:auth コマンドを実行し、認証関連のルーティングやビューの設定をおこないます。

$ php artisan make:auth
Authentication scaffolding generated successfully.

3.8.3. migrate コマンド

migrate コマンドを実行し、データベーステーブル群の作成をおこないます。

$ php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table

3.8.4. ユーザー登録

ビルトイン Web サーバーを再起動し(php artisan serve)、ユーザー登録ページ(http://127.0.0.1:8000/register)にアクセスしてユーザー登録をおこなってください。

user-registration.png

3.9. 認可エンドポイント

3.9.1 認可エンドポイントにおける処理の流れ

クライアントアプリケーションから認可リクエストを受け取った認可エンドポイントは、すぐに認可コードやアクセストークンを発行するわけではありません。発行前に、ユーザーに確認を取ります。そのため、認可リクエストを受け取ったあとに認可エンドポイントが返すのは、認可コードやアクセストークンではなく、認可画面(HTML)です。

ユーザーは、認可画面で認可リクエストの内容を確認し、認可リクエストを承認するか拒否するかを判断して、承認ボタンか拒否ボタンかを押します。すると、その応答は認可サーバーに送られます。その応答を受けてはじめて、認可サーバーは認可コードやアクセストークンを発行します。

この処理の流れを図にすると次のようになります。

flow-at-authorization-endpoint.png
注:図中の「認可決定エンドポイント」という呼称は便宜的にここでそう呼んでいるだけであり、一般的にコンセンサスの取れた用語ではありません。

上図が示すように、実は、認可リクエストを処理するために、認可サーバー側では少なくとも二つ以上のエンドポイントを用意しなければなりません。しかしながら、仕様書では明示的にその点については言及しておらず、概念的にそれらのエンドポイントを全部ひっくるめて認可エンドポイントと呼んでいます。

参考までに、RFC 6749 で定められている部分だけ色付けすると、下図のようになります。

flow-at-authorization-endpoint-mentioned-in-oauth.png

3.9.2. 認可リクエストパラメーター

RFC 6749 で定義されているアクセストークン発行フローのうち、認可エンドポイントを使うのは次の二つです。

それぞれのフローにおいて、クライアントアプリケーションがどのような HTTP リクエストを投げるべきかについては、RFC 6749 の「4.1.1. Authorization Request」と「4.2.1. Authorization Request」に書かれています。

リクエストパラメーター群は共通で、下表の通りです。

パラメーター 要否 説明
response_type 必須 応答種別
client_id 必須 クライアント識別子
redirect_uri 条件 応答返信先
scope 任意 要求する権限
state 推奨 状態変数

response_type は、認可エンドポイントからの応答として何を期待しているかを示しており、code であれば認可コード、token であればアクセストークンとなります。別の見方をすると、response_type はどのフローを使うかを示しており、code であれば認可コードフロー、token であればインプリシットフローとなります。

しかしながら、response_type の値が code なのか token なのかでフローを判断できる時代は、OpenID Connect の登場とともに終焉を迎えました。詳細は『OAuth 2.0 Multiple Response Type Encoding Practices』で定義されていますが、現在では、response_type の値は、「code, token, id_token の任意の組み合わせ」もしくは「none」となっています。

OpenID Connect によって、response_type の値が複雑になっただけではなく、多くのリクエストパラメーターも追加されました。OpenID Connect Core 1.0 では、次のようなパラメーター群が追加されています。

  • response_mode
  • nonce
  • display
  • prompt
  • max_age
  • ui_locales
  • id_token_hint
  • login_hint
  • acr_values
  • claims_locales
  • claims
  • request
  • request_uri
  • registration

なお、認可エンドポイントに新たなリクエストパラメーターを追加する仕様は OpenID Connect だけではありません。例えば RFC 7636(Proof Key for Code Exchange by OAuth Public Clients)は、次のパラメーター群を追加しています。

  • code_challenge
  • code_challenge_method

3.9.3. Authlete に認可リクエスト解析処理を任せる

RFC 6749 くらいなら何とか読めるのですが、『OpenID Connect Core 1.0』がとにかく長く、相当気合いが入っていないと読破できません。加えて、前提知識として下表の仕様群も知っておかなければなりません。そして、関連仕様はこれだけではありません。

仕様書 省略形 名称
RFC 7515 JWS JSON Web Signature
RFC 7516 JWE JSON Web Encryption
RFC 7517 JWK JSON Web Key
RFC 7518 JWA JSON Web Algorithms
RFC 7519 JWT JSON Web Token

仕様が膨大で、さらに、ちょっとした実装ミスがセキュリティー問題に直結するので、一から認可サーバー・OpenID プロバイダーを自作するのは大変です。そこで、Authlete の出番です。

Authlete をバックエンドの仕組みとして利用する認可サーバーの認可エンドポイントは、クライアントアプリケーションから受け取った認可リクエストのパラメーター群を、解釈せずにそのまま Authlete の /api/auth/authorization API に渡します。解釈作業は全て /api/auth/authorization API の実装がおこないます。

/api/auth/authorization API からの応答には次のものが含まれます。

  1. 認可リクエストに関する情報
  2. 認可エンドポイントの実装が次に取るべき行動
  3. クライアントアプリケーションに返すべき応答の内容(認可リクエストに問題がある場合)

例えば、認可リクエストに client_id パラメーターが含まれていない場合、Authlete の /api/auth/authorization API は、「クライアントに 400 Bad Request を返してください。エラーメッセージの内容としては◯◯を使ってください。」という応答を返します。認可リクエストが正常であれば、「認可リクエストに問題はありませんでした。ユーザーとインタラクティブにやりとりして、認可リクエストに対する同意をとってください。その後、同意結果に応じて、/api/auth/authorization/issue API もしくは /api/auth/authorization/fail API を呼んでください。」といった応答を返します。

クライアントアプリケーションから認可リクエストを受け取った直後の処理は次のようになります。

public function __invoke(AuthleteApi $api, Request $request)
{
    // 認可リクエストのパラメーター群を Authlete の
    // /api/auth/authorization API に渡す。
    $response = $this->callAuthorizationApi($api, $request);

    // 次に認可エンドポイントが取るべき行動
    $action = $response->getAction();

    switch ($action)
    {
        case AuthorizationAction::$INTERACTION:
            // ユーザーとインタラクションして認可リクエストを処理する。
            // 認可画面を返す。
            return $this->handleInteraction(
                              $api, $request, $response);

        case AuthorizationAction::$NO_INTERACTION:
            // ユーザーとインタラクションせずに認可リクエストを処理する。
            // ここにくるのは、認可リクエストに 'prompt=none' が
            // 含まれていた場合のみ。
            return $this->handleNoInteraction(
                              $api, $request, $response);

        default:
            // エラー応答を返す。
            return $this->handleError(
                              $api, $request, $response);
    }
}

3.9.4. 認可エンドポイントの実装

php artisan authlete:authorization-server により、認可エンドポイントの実装ははじめから動く状態になっています。次のファイル群が起点となりますので、必要に応じて実装を参照してください。

  • app/Http/Controllers/Authlete/AuthorizationController
  • app/Http/Controllers/Authlete/AuthorizationDecisionController
  • resources/views/authlete/authorization.blade.php
  • public/css/authlete/authorization.css

拡張性を考慮して、実装の階層化と部品化がされています。以下、実装を読み解く際のヒントです。

  1. app/Http/Controllers/Authlete に置かれたコントローラー群は、\Authlete\Laravel\Controller 内のコントローラーのいずれかを拡張している。
  2. そのため、単純な動作変更は、app/Http/Controllers/Authlete のコントローラーで、親クラスのメソッドをオーバーライドすることにより実現できる。
  3. オーバーライド可能なメソッドの中には、XxxSpi インターフェースの実装を返すものがある。\Authlete\Laravel\Handler\Spi 内に SPI(Service Provider Interface)群が幾つか定義されており、大きなカスタマイズポイントとなっている。
  4. SPI のデフォルト実装が \Authlete\Laravel\Handler\Spi 内に存在しており、それらは Laravel 認証を使う実装になっている。Laravel 認証を使わない場合、SPI を自分で実装することになる。
  5. SPI は、ハンドラーの動作を調整するために使われる。ハンドラー群は \Authlete\Laravel\Handler に置かれている。
  6. ハンドラーを使わずに、Authlete API を直接叩いてもかまわない。AuthleteApi インターフェースが authlete/authlete ライブラリで定義されており、Authlete API へのコールはこのインターフェースに集約されている。authlete/authlete ライブラリは Laravel ではない PHP フレームワークでも使うことができる。

3.9.4.1. ユーザー認証時刻

OpenID Connect において、認証後許容経過時間(Maximum Authentication Age)というものがあります。また、認証時刻を表す auth_time クレームというものもあります。これらの処理のため、ユーザーが認証された時刻を知る必要があります。しかし、php artisan make:auth 直後の Laravel 認証では、ユーザー認証時刻を簡単に知ることはできません。

Laravel 認証に手を加えてユーザー認証時刻を記録することは可能で、方法を解説する記事も幾つも存在します。authlete/authlete-laravel ライブラリの php artisan authlete:authorization-server コマンドで、そのような Laravel 認証拡張を自動実装することも技術的には可能です。しかし、それをやってしまうと、php artisan authlete:authorization-server が Laravel 認証前提のコマンドになってしまいます。

そこで、authlete/authlete-laravel ライブラリでは、ユーザー認証時刻を取得する処理を実装はせず、カスタマイズポイントだけを用意しています。app/Http/Controllers/Authlete 内の AuthorizationController.phpAuthorizationDecisionController.php で定義されている AuthorizationController クラスと AuthorizationDecisionController クラスのgetUserAuthenticatedAt() メソッドがそれです。初期実装は次のようになっています。

protected function getUserAuthenticatedAt(User $user, Request $request)
{
    return parent::getUserAuthenticatedAt($user, $request);
}

このメソッドの実装を変更し、ユーザー認証時刻を Unix エポック(1970 年 1 月 1 日)からの経過秒数で返すようにすれば、認証後許容経過時間や auth_time クレームに対応できるようになります。

3.10. 認可リクエスト

認可エンドポイントが実装できたので、リクエストを投げてみましょう。

3.10.1. インプリシットフローによるアクセストークン要求

認可エンドポイントのみで完結するインプリシットフローでアクセストークンを要求してみます。RFC 67494.2.1. Authorization Request によれば、必須パラメーターは response_typeclient_id です。インプリシットフローなので、response_typetoken で固定です。client_id には Authlete から発行されたクライアント ID を使います。結果、次のような認可リクエストになります。

インプリシットフローによるアクセストークン要求
http://127.0.0.1:8000/authorization?response_type=token&client_id={クライアントID}

この認可リクエストをブラウザのアドレスバーに入力すると、認可画面が表示されます。

authorization-page_implicit-flow.png

Login ID と Password には、「3.8.4. ユーザー登録」で登録したユーザーのメールアドレスとパスワードを入力してください。

ちなみに、(1)既にログイン済み、(2)認証後許容経過時間を経過していない(リクエストの max_age パラメーターやクライアントの default_max_age 設定値の影響を受ける)、(3)リクエストの prompt パラメーターが login を含んでいない、という条件が満たされると、ログインフォームは表示されません。

Authorize ボタンを押してフォームを送信すると、認可サーバーから、ステータスコードが「302 Found」の HTTP レスポンスが返ってきます。

HTTP/1.1 302 Found
Location: {リダイレクトURI}?#access_token={アクセストークン}&...
Cache-Control: no-store
Pragma: no-cache

この HTTP レスポンスを受け取ると、ブラウザは、そのレスポンスの Location ヘッダーに書かれている URL に遷移します。

Location ヘッダーに書かれている URL は、クライアントアプリケーションが事前に認可サーバーに登録していた URL です。これは、リダイレクト URI と呼ばれます。クライアントアプリケーションを Authlete に登録後、リダイレクト URI の設定を変更していなければ、その値は、「https://api.authlete.com/api/mock/redirection/{サービスAPIキー}」ですので、ブラウザは Authlete サーバーの「/api/mock/redirection/{サービスAPIキー}」に遷移することになります。

Authlete サーバーの「/api/mock/redirection/{サービスAPIキー}」は、受け取ったパラメーター群を見やすく表示してくれます。

redirect_uri-implicit_flow.png

上記のスクリーンショットで access_token の隣にある pQ45 で始まる値が発行されたアクセストークンです。

3.10.2. インプリシットフローによる ID トークン要求

せっかく OpenID Connect がサポートされているので、ID トークン を要求してみましょう。

認可エンドポイントから ID トークンを直接発行してもらうためには、response_typeid_token を含めます。この際のリクエストの仕様は、OpenID Connect Core 1.0 の「3.2.2.1. Authentication Request」に記述されています。

仕様書を読んでいくと、必須パラメーターがいろいろあることが分かります。

パラメーター 要否 説明
response_type 必須 id_token を含める。
client_id 必須 クライアント ID
redirect_uri 必須 リダイレクト URI
scope 必須 openid を含める。
nonce 必須 ノンス

RFC 6749 では、ある条件下では redirect_uri パラメーターは省略可能でしたが、OpenID Connect では必須となります。

OpenID Connect リクエストでは、scope パラメーターに openid を含めなければなりません。なお、OpenID Connect では、openid 以外にも、幾つか標準スコープ名を定義しています。それらのうち、profileemailaddressphone は、ID トークンに含めるクレームを制御します。詳細は OpenID Connect Core 1.0 の「5.4. Requesting Claims using Scope Values」で定義されています。のちほど挙げるリクエスト例では、openid に加えて、profileemailscope パラメーターに含めています。

OpenID Connect であっても、認可コードフローであれば、nonce はオプショナルです(3.1.2.1. Authentication Request)。しかし、インプリシットフローの場合は nonce は必須となります(3.2.2.1. Authentication Request)。

上記をまとめますと、インプリシットフローによる ID トークン要求リクエストは次のようになります。

インプリシットフローによる ID トークン要求
http://127.0.0.1:8000/authorization?response_type=id_token&client_id={クライアントID}&redirect_uri=https://api.authlete.com/api/mock/redirection/{サービスAPIキー}&scope=openid+email+profile&nonce={ノンス}

このリクエストを投げると、次のような認可画面が表示されます。

authorization-page_response_type-id_token.png

ログイン ID とパスワードを入力して Authorize ボタンを押すと、ID トークンが発行されます。

redirect_uri-id_token.png

3.11. トークンエンドポイント

3.11.1. トークンエンドポイントを利用するフロー

トークンエンドポイントは、インプリシットフロー以外のフロー、具体的には次のフローで使用されます。

3.11.2. トークンリクエストパラメーター

どのフローにも共通して grant_type リクエストパラメーターがあり、その値によってどのフローなのかが判別されます。例えば、認可コードフローであれば grant_type の値は authorization_code になり、リソースオーナーパスワードクレデンシャルズフローでは password になります。

それぞれのフロー毎に必須となるリクエストパラメーターは異なります。例えば、認可コードフローであれば code パラメーターが必須ですし(4.1.3. Access Token Request)、リソースオーナーパスワードクレデンシャルズフローでは usernamepassword が必須となります(4.3.2. Access Token Request)。

この他、RFC 7636 をサポートするのであれば、トークンエンドポイントは code_verifier リクエストパラメーターを認識する必要があります。

3.11.3. Authlete にトークンリクエスト処理を任せる

認可エンドポイントの実装と同様に、トークンエンドポイントの実装も、クライアントアプリケーションから受け取ったトークンリクエストのパラメーター群を、解釈せずに Authlete の /api/auth/token API に渡します。/api/auth/token API は、トークンリクエストを解釈し、アクセストークンや ID トークンの発行をおこないます。

リソースオーナーパスワードクレデンシャルズフローの場合のみ、/api/auth/token API だけでは処理が完結せず、トークンエンドポイントは、続けて /api/auth/token/issue API もしくは /api/auth/token/fail API を呼ぶことになります。というのは、リソースオーナーパスワードクレデンシャルズフローでは、usernamepassword の値でユーザー認証をおこなう必要があるのですが、Authlete はユーザー認証をおこなわないので、ユーザー認証処理をおこなってもらうため、トークンエンドポイントの実装に処理を一度戻す必要があるためです。

クライアントアプリケーションからトークンリクエストを受け取った直後の処理は次のようになります。

public function handle(Request $request)
{
    // Authlete の /api/auth/token API にトークンリクエストの
    // パラメーター群を渡す。
    $response = $this->callTokenApi($request);

    // トークンエンドポイントの実装が次に取るべき行動
    $action = $response->getAction();

    // クライアントアプリケーションに返すレスポンスの内容。
    // フォーマットは $action の値によって異なる。
    $content = $response->getResponseContent();

    switch ($action)
    {
        // クライアント認証が不正
        case TokenAction::$INVALID_CLIENT:
            // 401 Unauthorized
            return ResponseUtility::unauthorized(
                       self::$CHALLENGE, $content);

        // Authlete 側でエラー発生
        case TokenAction::$INTERNAL_SERVER_ERROR:
            // 500 Internal Server Error
            return ResponseUtility::internalServerError($content);

        // トークンリクエストが不正
        case TokenAction::$BAD_REQUEST:
            // 400 Bad Request
            return ResponseUtility::badRequest($content);

        // リソースオーナーパスワードクレデンシャルズフロー
        case TokenAction::$PASSWORD:
            // username と password によりユーザー認証をおこない、
            // /api/auth/token/issue API か /api/auth/token/fail
            // API のどちらかを呼ぶ。
            return $this->handlePassword($response);

        // トークンリクエストが正常
        case TokenAction::$OK:
            // 200 OK
            // アクセストークンを含む応答を返す。
            return ResponseUtility::okJson($content);

        // $action の値が未知
        default:
            // 500 Internal Server Error.
            // これは起こってはいけない。
            return $this->unknownAction('/api/auth/token');
    }
}

3.11.4. トークンエンドポイントの実装

php artisan authlete:authorization-server により、トークンエンドポイントの実装ははじめから動く状態になっています。次のファイルが起点となりますので、必要に応じて実装を参照してください。

  • app/Http/Controllers/Authlete/TokenController

3.12. 認可コードフロー

さて、ここまでの処理で、認可エンドポイントとトークンエンドポイントの両方の実装が終わりました。これでやっと、本命の認可コードフローを試すことができます。

認可コードフローでは、まず、認可エンドポイントから認可コードが発行されます。クライアントアプリケーションは、その認可コードをトークンエンドポイントに提示することにより、交換でアクセストークンを受け取ります。なお、認可コードの寿命は短いので(最長10分が推奨と仕様書に書かれている)、認可コードを受け取ったあとは、間をおかずにトークンリクエストをおこなわなければなりません。

認可コードフローを図にすると、次のようになります。

authorization-code-flow.png

参考までに、RFC 6749 で定められている部分だけ色付けすると、下図のようになります。

authorization-code-flow-mentioned-in-oauth.png

3.12.1. 認可コードフローにおける認可リクエスト

認可コードフローの認可リクエストでは、response_type の値が code となります。

認可コードフローにおける認可リクエスト
http://127.0.0.1:8000/authorization?response_type=code&client_id={クライアントID}

これまで同様、認可画面で Authorize ボタンを押すと、リダイレクト URI 先に遷移します。表示された画面で code の値として表示されているものが、発行された認可コードです。

redirect_uri-code.png

3.12.2. 認可コードフローにおけるトークンリクエスト

認可コードフローのトークンリクエストでは、grant_type の値を authorization_code とします。必須パラメーター code には、認可エンドポイントから発行された認可コードを指定します。また、クライアント認証をおこなわない場合、client_id パラメーターも加えます。

トークンエンドポイントは GET メソッドではなく POST メソッドでリクエストを受け付けるので、ブラウザのアドレスバーへの入力ではトークンリクエストを投げることはできません。curl コマンドなどを用いて POST メソッドでトークンリクエストを投げるようにしてください。

上記をまとめますと、認可コードフローにおけるトークンリクエストは次のようになります。

$ curl http://127.0.0.1:8000/api/token \
    -d grant_type=authorization_code \
    -d code={認可コード} \
    -d client_id={クライアントID}

トークンリクエストが成功すると、次のような JSON が返ってきます。

{
  "access_token":"0l1vrjWLDM_hzBXRy2Ydz3w4ij6G893Hjqr5W8_rBEA",
  "refresh_token":"lH9Vi6Lpzs6pz7IJq2xm0de--Bf-Rifi3vWmjgVbVdA",
  "scope":null,
  "token_type":"Bearer",
  "expires_in":86400
}

注:出力は見やすいように整形してあります。

この JSON 内の access_token の値が、発行されたアクセストークンです。

3.13. 認可サーバーの基本動作確認完了

認可コードフローを試すことにより、認可エンドポイントとトークンエンドポイントの両方を使いました。これにより、認可サーバーの基本動作を確認することができました。:clap_tone2::clap_tone2::clap_tone2:

4. リソースサーバーの実装

4.1. Laravel プロジェクト作成

リソースサーバー用に、別途 Laravel プロジェクトを生成します。

$ laravel new resource-server
$ cd resource-server

4.2. authlete-laravel ライブラリ導入

$ composer require authlete/authlete-laravel

認可サーバーのときと同様、Laravel のバージョンが 5.5 よりも古い場合、AuthleteServerProvider を手作業で config/app.php に追加する必要があります。

4.3. authlete:resource-server コマンド

authlete-laravel ライブラリを導入することにより、Artisan 用のコマンド authlete:resource-server が使えるようになるので、実行してください。

$ php artisan authlete:resource-server

authlete:resource-server コマンドにより次の処理がおこなわれます。

  1. 設定ファイル config/authlete.php を生成(既に存在する場合は上書きしない)
  2. routes/api.php にルーティング設定を追加
  3. app/Http/Controllers/AuthleteUserInfoController.php を生成

authlete:resource-server コマンドの動作詳細については、AuthleteResourceServerCommand.php を参照してください。

4.4. Authlete 設定ファイル

php artisan authlete:resource-server により、設定ファイル config/authlete.php が生成されます。ただし、既に存在する場合は上書きしません。認可サーバーのときと同様、Authlete から発行されたサービス API キーとサービス API シークレットを設定してください。

4.5. API の実装

API を作り、それをアクセストークンで保護する実験をしてみましょう。

4.5.1. /api/time API

まず、現在時刻に関する情報を JSON 形式で返すという単純な動作をする API を作成してみます。app/Http/Controllers/TimeController.php ファイルを新規作成し、次の内容を書き込んでください。

<?php
namespace App\Http\Controllers;

class TimeController extends Controller
{
    public function __invoke()
    {   
        return response()->json(getdate());
    }   
}
?>

次に、routes/api.php を開いて、TimeController/api/time にマッピングします。パス名の先頭には /api をつけないでください。

Route::get('/time', '\App\Http\Controllers\TimeController');

動作確認をしてみましょう。まず、php artisan serve でリソースサーバーを起動します。下記の例では、認可サーバーのポート番号とぶつからないよう、--port オプションでポート番号を指定しています。

$ php artisan serve --port=8001

/api/time API をたたいてみます。

$ curl http://127.0.0.1:8001/api/time

次のような JSON が返ってきたら成功です。

{
  "seconds":20,
  "minutes":14,
  "hours":15,
  "mday":22,
  "wday":2,
  "mon":5,
  "year":2018,
  "yday":141,
  "weekday":"Tuesday",
  "month":"May",
  "0":1527002060
}

注:出力は見やすいように整形してあります。

4.5.2. アクセストークンの抽出

RFC 6750 で定義されている方法でアクセストークンを取り出すのは、\Illuminate\Http\Request クラスのインスタンスがあれば、簡単です。

// RFC 6750, 2.1. Authorization Request Header Field
//   Authorization ヘッダーからアクセストークンを取り出す。
$accessToken = $request->bearerToken();

// RFC 6750, 2.2. Form-Encoded Body Parameter
//   Form パラメーターからアクセストークンを取り出す。
$accessToken = $request->input('access_token');

// RFC 6750, 2.3. URI Query Parameter
//   Query パラメーターからアクセストークンを取り出す。
$accessToken = $request->query('access_token');

authlete/authlete-laravel ライブラリの WebUtility クラスには、上記と同等のことをおこなう extractAccessTokenFrom*() メソッド群が用意されています。

// RFC 6750, 2.1. Authorization Request Header Field
//   Authorization ヘッダーからアクセストークンを取り出す。
$accessToken = WebUtility::extractAccessTokenFromHeader($request);

// RFC 6750, 2.2. Form-Encoded Body Parameter
//   Form パラメーターからアクセストークンを取り出す。
$accessToken = WebUtility::extractAccessTokenFromBody($request);

// RFC 6750, 2.3. URI Query Parameter
//   Query パラメーターからアクセストークンを取り出す。
$accessToken = WebUtility::extractAccessTokenFromQuery($request);

また、アクセストークンが見つかるまで、上記三つを順番に試みる extractAccessToken() メソッドもあります。

// RFC 6750 で定義されている三つの方法を、アクセストークンが
// 見つかるまで順番に試す。
$accessToken = WebUtility::extractAccessToken($request);

4.5.3. AccessTokenValidator

authlete/authlete-laravel ライブラリには、アクセストークンの有効性を確認するためのクラス、AccessTokenValidator が用意されています。

コンストラクターに AuthleteApi インターフェースの実装を渡してインスタンスを作り、setAccessToken() メソッドで対象となるアクセストークンをセットし、setRequiredScopes() 等でチェック条件を指定したあと、validate() メソッドを呼ぶことにより有効性をチェックすることができます。

次の例では、アクセストークンの有効期限が切れていないことと、スコープとして少なくとも emailprofile を持っていることを確認しています。

// チェック対象のアクセストークン
$accessToken = ...;

// AuthleteApi インターフェースを実装したインスタンス
$api = ...;

// AccessTokenValidator を生成する。
$validator = new AccessTokenValidator($api);

// チェック対象のアクセストークンを設定する。
$validator->setAccessToken($accessToken);

// API にアクセスするために必要となるスコープ群
$requiredScopes = array('email', 'profile');
$validator->setRequiredScopes($requiredScopes);

// 有効性をチェック
$valid = $validator->validate();

validate() メソッドの実装は、内部で Authlete の /api/auth/introspection API を呼び出します。この API コールが成功していれば、validate() 実行後、getIntrospectionResponse() メソッドで /api/auth/introspection API からのレスポンスを取得することができます。

// /api/auth/introspection API からのレスポンス。
// \Authlete\Dto\IntrospectionResponse クラスのインスタンス。
$introspectionResponse = $validator->getIntrospectionResponse();

getIntrospectionResponse() メソッドが返すのは IntrospectionResponse クラスのインスタンスです。このインスタンスから、アクセストークンに紐づくクライアント ID やユーザー識別子などの情報が得られます。

// アクセストークンの発行対象のクライアントアプリの識別子
$clientId = $introspectionResponse->getClientId();

// アクセストークンの発行を許可したユーザーの識別子
$subject = $introspectionResponse->getSubject();

// アクセストークンに許可されたスコープ群
$scopes = $introspectionResponse->getScopes();

アクセストークンが無効であったり、/api/auth/introspection API コールそのものが失敗していた場合、getErrorResponse() はクライアントに返すべきエラーレスポンスを返します。

// クライアントに返すべきエラーレスポンス。
// \Illuminate\Http\Response クラスのインスタンス
$errorResponse = $validator()->getErrorResponse();

getErrorResponse() メソッドが返すのは \Illuminate\Http\Response クラスのインスタンスなので、そのままコントローラーからのレスポンスとして返すことができます。

// アクセストークンが有効でなければ、
if (!$validator->validate())
{
    // RFC 6750 に準拠するエラーレスポンスを返す。
    return $validator->getErrorResponse();
}

AccessTokenValidator には create() という static メソッドがあり、AccessTokenValidator のインスタンスを簡単に作れるようになっています。

/**
 * Create a validator.
 *
 * The access token is extracted from the `Authorization` header of the
 * request. Note that the `access_token` query parameter and the
 * `access_token` form parameter are NOT referred to even if they exist.
 *
 * @param AuthleteApi $api
 *     An implementation of the `AuthleteApi` interface.
 *
 * @param Request $request
 *     A request from a client application.
 *
 * @param string[] $requiredScopes
 *     Scopes which are required to access the protected resource endpoint.
 *     This argument is optional and its default value is `null`.
 *
 * @param string $requiredSubject
 *     Subject (= unique user identifier) which is required to be
 *     associated with the access token. This argument is optional and
 *     its default value is `null`.
 *
 * @return AccessTokenValidator
 *     A new `AccessTokenValidator` instance.
 */
public static function create(
    AuthleteApi $api, Request $request, array $requiredScopes = null,
    $requiredSubject = null)

先ほど挙げた AccessTokenValidator の生成手順を create() メソッドを使っておこなうと次のようになります。リクエストからのアクセストークン抽出処理は create() メソッド内部でおこなわれます。

$validator =
    AccessTokenValidator::create(
        $api, $request, ['email', 'profile']);

4.5.4. アクセストークンによる保護

では、/api/time API をアクセストークンで保護してみましょう。

TimeController.php の内容を次のように書き換えてください。

<?php
namespace App\Http\Controllers;

use Authlete\Api\AuthleteApi;
use Authlete\Laravel\Web\AccessTokenValidator;
use Illuminate\Http\Request;

class TimeController extends Controller
{
    public function __invoke(Request $request, AuthleteApi $api)
    {
        $validator = AccessTokenValidator::create(
            $api, $request, ['email', 'profile']);

        if (!$validator->validate())
        {   
            return $validator->getErrorResponse();
        }   

        return response()->json(getdate());
    }   
}
?>

これにより、/api/time API はアクセストークンで保護されました。

4.6. API の試験

4.6.1. アクセストークン無し

php artisan serve --port=8001 で Web サーバーを再起動後、まず、/api/time にアクセストークン無しでアクセスしてみましょう。

$ curl http://127.0.0.1:8001/api/time
$ # 何も出力されない!

おそらく画面には何も出力されないと思います。 /api/time400 Bad Request を返していて、エラーメッセージは WWW-Authenticate ヘッダーに含まれているのですが(RFC 6750, 3. The WWW-Authenticate Response Header Field を参照)、それらの情報が画面に表示されません。そこで、curl に -v オプションをつけて再度実行してみます。

$ curl -v http://127.0.0.1:8001/api/time
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8001 (#0)
> GET /api/time HTTP/1.1
> Host: 127.0.0.1:8001
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 400 Bad Request
< Host: 127.0.0.1:8001
< Date: Tue, 22 May 2018 16:36:16 +0000
< Connection: close
< X-Powered-By: PHP/7.1.14
< Cache-Control: no-store, private
< Date: Tue, 22 May 2018 16:36:16 GMT
< Pragma: no-cache
< WWW-Authenticate: Bearer error="invalid_token",error_description=
  "[A057301] The request does not contain a valid access token.",
  error_uri="https://www.authlete.com/documents/apis/result_codes#A057301"
< Content-Type: text/html; charset=UTF-8
< X-RateLimit-Limit: 60
< X-RateLimit-Remaining: 59
< 
* Closing connection 0
$

注:出力を見やすくするため、WWW-Authenticate の行に改行を幾つか入れています。

今度は、HTTP ステータスが 400 Bad Request であることや、WWW-Authenticate ヘッダーにエラーメッセージが含まれていることが分かります。エラーコードは invalid_token で、エラーメッセージは "The request does not contain a valid access token."(リクエストが有効なアクセストークンを含んでいない)となっています。

4.6.2. スコープが足りないアクセストークン

今度はアクセストークンを添えて /api/time API にアクセスしてみます。ただし、スコープに emailprofile を含まないアクセストークンを用いて、エラーとしてはじかれることを確認します。

「3.10.1. インプリシットフローによるアクセストークン要求」に書かれている手順でアクセストークンを生成すると、何もスコープを持たないアクセストークンが生成されるので、それを用いてみましょう。

アクセストークンの指定方法として RFC 6750, 2.1. Authorization Request Header Field を用いる場合、curl に次のようなオプションをつけます。

-H 'Authorization: Bearer {アクセストークン}'

※:AccessTokenValidator::create() メソッドはアクセストークンを抽出する際、RFC 6750, 2.1 の方法しか試みません。

では、やってみます。

$ curl -v http://127.0.0.1:8001/api/time \
       -H 'Authorization: Bearer YGhwQUO-9GC5NmnfJ9kVHMgSsnkiTFcg5hoRt5NK5DM'
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8001 (#0)
> GET /api/time HTTP/1.1
> Host: 127.0.0.1:8001
> User-Agent: curl/7.54.0
> Accept: */*
> Authorization: Bearer YGhwQUO-9GC5NmnfJ9kVHMgSsnkiTFcg5hoRt5NK5DM
> 
< HTTP/1.1 403 Forbidden
< Host: 127.0.0.1:8001
< Date: Tue, 22 May 2018 17:05:11 +0000
< Connection: close
< X-Powered-By: PHP/7.1.14
< Cache-Control: no-store, private
< Date: Tue, 22 May 2018 17:05:11 GMT
< Pragma: no-cache
< WWW-Authenticate: Bearer error="insufficient_scope",error_description=
  "[A064301] No scopes are associated with the access token.",
  error_uri="https://www.authlete.com/documents/apis/result_codes#A064301",
  scope="profile email"
< Content-Type: text/html; charset=UTF-8
< X-RateLimit-Limit: 60
< X-RateLimit-Remaining: 59
< 
* Closing connection 0
$

注:出力を見やすくするため、WWW-Authenticate の行に改行を幾つか入れています。

想定通り、今度もエラーとなりました。エラーコードは insufficient_scope になっており、アクセストークンが API アクセスに必要なスコープ群を持っていないことを示しています。ちなみに、この insufficient_scope は RFC 6750 の「3.1. Error Codes」で定義されている値です。

また、WWW-Authenticate ヘッダーの値の末尾には scope="profile email" と書かれており、この API にアクセスするためには profile スコープと email スコープが必要だということが分かります。

ここまでで気が付かれたかもしれませんが、RFC 6750 にしっかり準拠しようとすると、API がエラーを返す時は、細かいルールにのっとって WWW-Authenticate ヘッダーの値を設定しなければなりません。正直言ってこれは面倒です。ですが、Authlete を使うと、WWW-Authenticate ヘッダーに設定すべき値の生成も Authlete サーバー側でやってくれるので、RFC 6750 への準拠も楽です。これが、OAuth 標準のイントロスペクション API である RFC 7662(OAuth 2.0 Token Introspection)と Authlete の /api/auth/introspection API の違いです。標準のイントロスペクション API は、アクセストークンの情報を返すだけであり、アクセストークンの有効性チェックやエラーメッセージの生成などはやってくれません。

tweet-about-authlete-introspection.png

4.6.3. 有効なアクセストークン

最後に、十分なスコープを持ったアクセストークンを添えて /api/time API にアクセスしてみます。

まず、emailprofile をスコープとして持つアクセストークンを作成します。インプリシットフローで作成する場合は、次の URL をブラウザのアドレスバーに入力してください。「3.10.1. インプリシットフローによるアクセストークン要求」とは異なり、今度は scope パラメーターがついています。

scope パラメーター付きインプリシットフロー
http://127.0.0.1:8000/authorization?response_type=token&client_id={クライアントID}&scope=email+profile

全てコマンドラインで済ませたければ、リソースオーナーパスワードクレデンシャルズフローでもかまいません。

リソースオーナーパスワードクレデンシャルズフローによるアクセストークン要求

$ curl -v http://127.0.0.1:8000/api/token \
  -d grant_type=password \
  -d username={ログインID} \
  -d password={パスワード} \
  -d client_id={クライアントID} \
  -d scope='email profile'
{
  "access_token":"2Dde16KZPp1PDmBClM8bQJMmomQUYPjsYLfcB2vqcmU",
  "refresh_token":"y5SL5DAFY3rPfRORtgSj31_F2WYw-lDYkXnBY6rgH--",
  "scope":"email profile",
  "token_type":"Bearer",
  "expires_in":86400
}

では、生成したアクセストークンを添えて /api/time API にアクセスしてみましょう。

$ curl -v http://127.0.0.1:8001/api/time \
       -H 'Authorization: Bearer 2Dde16KZPp1PDmBClM8bQJMmomQUYPjsYLfcB2vqcmU'
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8001 (#0)
> GET /api/time HTTP/1.1
> Host: 127.0.0.1:8001
> User-Agent: curl/7.54.0
> Accept: */*
> Authorization: Bearer 2Dde16KZPp1PDmBClM8bQJMmomQUYPjsYLfcB2vqcmU
> 
< HTTP/1.1 200 OK
< Host: 127.0.0.1:8001
< Date: Tue, 22 May 2018 18:12:07 +0000
< Connection: close
< X-Powered-By: PHP/7.1.14
< Cache-Control: no-cache, private
< Date: Tue, 22 May 2018 18:12:07 GMT
< Content-Type: application/json
< X-RateLimit-Limit: 60
< X-RateLimit-Remaining: 59
< 
* Closing connection 0
{
  "seconds":7,
  "minutes":12,
  "hours":18,
  "mday":22,
  "wday":2,
  "mon":5,
  "year":2018,
  "yday":141,
  "weekday":"Tuesday",
  "month":"May",
  "0":1527012727
}

注:出力は見やすいように整形してあります。

今度は HTTP ステータス が 200 OK で、Content-Typeapplication/json、レスポンスボディーに期待した JSON が含まれています。/api/time API が正常応答を返したことが分かります。

4.7. ユーザー情報エンドポイント

OpenID Connect Core 1.0 の「5.3. UserInfo Endpoint」では、ユーザー情報を JSON 形式もしくは JWT 形式で返す『ユーザー情報エンドポイント』が定義されています。

authlete/authlete-laravel ライブラリには、ユーザー情報エンドポイントのデフォルト実装が含まれています。php artisan authlete:resource-server により、app/Http/Controllers/UserInfoController.php が作成され、routes/api.php にルーティング設定が追加されます。これにより、/api/userinfo でユーザー情報エンドポイントが提供されるようになります。このエンドポイントの動作確認をしてみることにします。

4.7.1. ユーザーデータベース

ユーザー情報エンドポイントは、ユーザーの情報を返すので、必然的にユーザーデータベースにアクセスする必要があります。

この記事に書かれた手順でユーザー登録をおこなっていれば、ユーザーデータベースは認可サーバーの database/database.sqlite ファイル内に存在します。しかし、リソースサーバーを作る際、別途 Laravel プロジェクトを作成したため、リソースサーバーからはそのユーザーデータベースにアクセスできません。

そこで、ここでは単純に、database.sqlite を認可サーバーの Laravel プロジェクトからリソースサーバーの Laravel プロジェクトにコピーすることにします。

$ cp authorization-server/database/database.sql \
     resource-server/database/

リソースサーバー側も SQLite を使うように、.env ファイルを設定してください。

DB_CONNECTION=sqlite
#DB_HOST=127.0.0.1
#DB_PORT=3306
#DB_DATABASE=homestead
#DB_USERNAME=homestead
#DB_PASSWORD=secret

4.7.2. アクセストークン

ユーザー情報エンドポイントにアクセスするためには、最低限 openid スコープを含むアクセストークンが必要です。

そこで、openid スコープを含むアクセストークンを生成してみましょう。なお、ユーザー情報エンドポイントから返される情報に影響があるので、email スコープと profile スコープも付けておくとよいでしょう。

openid スコープを含むアクセストークン要求
http://127.0.0.1:8000/authorization?response_type=token&client_id={クライアントID}&scope=openid+email+profile&redirect_uri=https://api.authlete.com/api/mock/redirection/{サービスAPIキー}

上記のリクエスト例は「3.10.2. インプリシットフローによる ID トークン要求」で用いたリクエストと似ていますが、response_type の値が id_token ではなく token になっています。

4.7.3. API アクセス

では、/api/userinfo にアクセスしてみましょう。

$ curl http://127.0.0.1:8001/api/userinfo \
       -H 'Authorization: Bearer e7wvFGThq2ZFcaXtDdzPhB7Oqbjr5SLfFtIiNmL0PSU'

つぎのような JSON が返ってきたら成功です。なお、クライアントアプリケーションの設定によっては JWT 形式で返ってくることがあります。

{
  "email":"john@example.com",
  "name":"John Smith",
  "iss":"https://authlete.com",
  "sub":"1",
  "aud":["7688853985532"],
  "exp":253402128000,
  "iat":1527022504
}

注:出力は見やすいように整形してあります。

4.8. アクセストークン情報のキャッシュ

「4.5.3. AccessTokenValidator」で紹介した AccessTokenValidator クラスの validate() メソッドは、呼ばれるたびに Authlete の /api/auth/introspection API にアクセスします。そのため、ネットワークレイテンシーの影響を受けます。

アクセストークン情報をリソースサーバー側でキャッシュすることで、毎回 Authlete サーバーと通信しなくてもよくなり、ネットワークレイテンシーの影響を軽減できます。Authlete 社も、お客様に対してアクセストークン情報のキャッシュを勧めています。

おわりに

PHP と Laravel 向けのライブラリ、authlete/authleteauthlete/authlete-laravel の開発には、当初想定した以上に時間がかかりました。Authlete のようなバックエンドシステムが存在していても、ここまで認可サーバー・OpenID プロバイダーの実装は大変なのかと、改めて実感しました。

しかしながら、その苦労のおかげで、php artisan authlete:authorization-server コマンドだけで認可サーバー・OpenID プロバイダーの基本機能の実装が完了する世界が実現できました。

人材確保、開発期間、開発費、品質、機能、再利用性、運用、最新仕様追従、等を考えると、Authlete 創業者の私が言うのもあれなんですが、手探りで認可サーバー・OpenID プロバイダーを自作するよりも、Authlete に月々のサブスクリプション・フィーを払ったほうが断然良いと思います! お問い合わせは sales@authlete.com まで!

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
87