Laravel5.5(LTS) + Passport 4.0でOauth2 Provider を構築したので、分かったことを記載する
Passport 6.0が出ていますが、LaravelのLTSに合わせて、Passport4.0で構築しているので、バージョンアップ後の挙動は対象外としています
1. **Laraport(4.0)**について
Laravel + Passport = Laraportを利用することで、League OAuth2サーバ上に構築された簡易版のOAuth Providerを構築することが出来ます
但し、あくまで簡易版ですので、いろいろな制限がありますし、きちんと認証サーバを作るということを考えた場合はおススメ出来ません。
その場合は、KeyCloak, MITREidConnect, Ping Identity, Gluu Server等 OpenID Certificateの認定を受けているサーバ製品がありますので、こちらをお勧めいたします
とはいえ、簡単に認証サーバを立ち上げて運用したいんだ!!って方には、1日で立ち上げられるLaraportは魅力的だと思います
出来ること
- Auth Code
- Implicit Grant
- Resource Owner Password Credentials Grant
- Client Credentials Grant
-
OAuth 2.0 Dynamic Client Registration Protocol
/oauth/clients で出来るのだと思うが、そこまでがちがちにしているわけではないと思われます -
基本セットとしては存在しませんが、token introspectという外部モジュールを使うことで提供することが出来ます
https://github.com/ipunkt/laravel-oauth-introspection/tree/master/src -
パーソナルアクセストークン
正直なところ、これが何を指しているのかが良く分からないです(利用シチュエーション)
新しいGrant的なものが追加されているんだろうなーとしか思っていないというか、使いどころが良く分かっていないので、あとで勉強してみます -
本来は、シングルログインにしたいらしいので、どこかのデバイスでトークンを取ると、イベントを投げて事前に取得したトークンを削除する仕組みがあるらしい
出来ないこと
-
client_idに数字以外を指定できない
データベーススキーマーとして、oauth_clientsを定義しています
client_idは、id:int(10) として定義しており、引数の client_id を利用する際に、この数字を指定しています
artisan コマンドを利用する際に、--name を指定できるので、両方利用可能かと思いましたが、コードを読む分には id のみで判断しているようなので多分使用できないでしょう。
※後ほど時間が出来たら追調査を行います
とはいえ、修正範囲は頑張れば対応できると思いますが、PRで取り込まれないと composer update 時にコードが戻ってしまうのと、Passport 6.0以降で出来るようになっている場合、悲しい結果になると思うので、どうするかの判断は、適時考えることをお勧めします -
client_id毎の詳細な設定
本来、access tokenやrefresh token はたまた、scopesに関しては、client_id単位で設定を行いたいところですが、そのようなデータベース設計はなされておりません。 -
openid-connect機能(たぶん、見当たらないので)
scopesに関してもシンプルなうえ、ID-Tokenを生成するような実装はなされていません。 -
single sign on
webでならば、cookieにセッションIDを入れることが出来るので、もしかしたら出来るのかも? -
accessTokenとscopeを指定しての、認可チェックを行うこと(API提供版)
オリジナルのLaraport(4.0)としては、introspectも無ければ、認可チェックを行う機能も存在しません。(RFC的には、もともと存在しておらず、introspect等で自身でチェックすることをお勧めしている、のかな?)
その為、サーバ側での認可チェック機能を持っていないので、Resource Server側では、外部コンポーネントを利用した Introspectを使って自身でscopesチェックを行うのが良いと思われます。
バグと思われること
1. refreshTokenが常に再生成される(※ League Oauth2 Serverのバグと思われる)
- /oauth/token を利用して refresh tokenからのaccess token再生成を行うと、refresh tokenも新たに再発行されてしまう
- ぶっちゃけこのままだと、以下の問題が出る
- expireを180日にしても、毎回再生成されているので、expireが来ない。その為、ユーザーからの再認証を改めてもらうタイミングが永久に来ないwww 意図的ってこともあるが
- tableをお掃除するバッチが存在しないので、refresh tokenを利用してのaccess token再生成のタイミングで毎回レコードが残るので、DBのレコード的に残念なことになる(revoked = trueではある)
- クライアント側としても、毎回refresh_tokenが再生成されるので、/oauth/token 時にrefresh tokenを改めて保存する必要がある
- そもそも論として、refresh_tokenの実体というか暗号化されたものを返していて、中にaccess tokenを含ませているのでレスポンスの値はrefresh_token_idが同じでも常に変更しているのは仕方ないけど、考える必要はあるよね
【現行仕様】
トークン再発行のタイミングで毎回新しいrefresh_token_idが発行され、かつ有効期限が伸びる
+---------+--------------+----------------------+---------------+-----------------------+-------------------------+--------------------------+
| user_id | access_token | access_token_revoked | refresh_token | refresh_token_revoked | access_token_created_at | refresh_token_expires_at |
+---------+--------------+----------------------+---------------+-----------------------+-------------------------+--------------------------+
| 3333 | 7f6f487fde | 1 | 6aa7c353d2 | 1 | 2018-06-19 10:46:53 | 2018-09-17 10:46:52 |
| 3333 | a2548b3e44 | 0 | 541837c20e | 0 | 2018-06-19 10:46:57 | 2018-09-17 10:46:57 |
| 3333 | a064338836 | 1 | 2c7634ec1f | 1 | 2018-06-19 10:48:15 | 2018-09-17 10:48:15 |
| 3333 | 4ace861197 | 0 | ff59c06947 | 0 | 2018-06-19 10:48:48 | 2018-09-17 10:48:48 |
+---------+--------------+----------------------+---------------+-----------------------+-------------------------+--------------------------+
ハッキングされたとしても再ログインする必要なく無期限に利用可能
refresh_tokenの有効期限を短めに設定し、定期的なアクセスを実施する人は再ログイン不要とするか?
※トークンは見やすくするため10桁で切っている。
【改修1 CustomRefreshTokenGrant】
同一refresh_token_idに新しいaccess_token_idを付け替える
+---------+--------------+----------------------+---------------+-----------------------+-------------------------+--------------------------+
| user_id | access_token | access_token_revoked | refresh_token | refresh_token_revoked | access_token_created_at | refresh_token_expires_at |
+---------+--------------+----------------------+---------------+-----------------------+-------------------------+--------------------------+
| 3333 | 5262a02b0f | 1 | NULL | NULL | 2018-06-19 11:02:05 | NULL |
| 3333 | 1354371492 | 1 | NULL | NULL | 2018-06-19 11:02:24 | NULL |
| 3333 | c5ab4ba364 | 1 | NULL | NULL | 2018-06-19 11:03:34 | NULL |
| 3333 | 2802044e87 | 1 | NULL | NULL | 2018-06-19 11:05:01 | NULL |
| 3333 | 37103a9ebe | 0 | a84a2e0b2a | 0 | 2018-06-19 11:06:09 | 2018-09-17 11:02:05 |
| 3333 | 119c4234de | 0 | 855c37408b | 0 | 2018-06-19 11:06:42 | 2018-09-17 11:02:24 |
+---------+--------------+----------------------+---------------+-----------------------+-------------------------+--------------------------+
refresh_tokenと旧access_tokenとの紐付けが失われるため、発行履歴を追えない。
【改修2 CustomRefreshTokenGrant2】
よく考えたら別に同一refresh_tokenを使い続ける必要はないので、新しく発行されるrefresh_tokenの期限を初回発行時のものにする 。
+---------+--------------+----------------------+---------------+-----------------------+-------------------------+--------------------------+
| user_id | access_token | access_token_revoked | refresh_token | refresh_token_revoked | access_token_created_at | refresh_token_expires_at |
+---------+--------------+----------------------+---------------+-----------------------+-------------------------+--------------------------+
| 3333 | 3343e4e25f | 1 | 23dd3faa6c | 1 | 2018-06-19 12:35:46 | 2018-09-17 12:35:46 |
| 3333 | 3177311559 | 1 | 4b05fd2254 | 1 | 2018-06-19 12:35:53 | 2018-09-17 12:35:53 |
| 3333 | 0c7b13385a | 1 | 9a59071e7d | 1 | 2018-06-19 12:36:12 | 2018-09-17 12:35:46 |
| 3333 | c1644a8c9b | 1 | edf70c690b | 1 | 2018-06-19 12:36:33 | 2018-09-17 12:35:53 |
| 3333 | 67495c7488 | 0 | b0e7dce52c | 0 | 2018-06-19 12:37:02 | 2018-09-17 12:35:53 |
| 3333 | 74e72c612a | 0 | 692beab82a | 0 | 2018-06-19 12:37:18 | 2018-09-17 12:35:46 |
+---------+--------------+----------------------+---------------+-----------------------+-------------------------+--------------------------+
refresh_tokenの有効期限は初回時のもののままで、発行タイミングの履歴も追える。
【現行仕様のままでトークンの期限を短くするとした場合】
LINEのOAuthAPIの場合
https://developers.line.me/ja/docs/social-api/managing-access-tokens/
現行のLaravelPassportのリフレッシュトークンと同様にaccess_token再発行のタイミングで作られる新しいrefresh_tokenの有効期限が伸び、refresh_token期限内であればセッション同様に生き続けられる。
access_tokenの有効期限は30日、refresh_tokenの有効期限はaccess_tokenの有効期限が切れてから10日
【結論】
access tokenに対して、refresh tokenを紐つけ直すこととした
refresh tokenを再生成させての履歴を追うなどは根本的に必要ないので。
但し、RefreshTokenGrant時に refresh tokenを毎回返却しているが、Laravel + Passportの場合は 中にaccess tokenを保持している関係で、サーバ側では同じrefresh token idを使っているにも関わらず暗号化したものを返しているので、クライアント側からみると値が変わって違うものを受けっとっていることになる
よって、RefreshTokenGrant時の戻り値として、refresh tokenは返さない様に修正して対応する
RFC的にもOptionだし
Laravelとしての認可チェック
Laravelは、middlewareを利用して認可チェックが出来るようになっています
Passportとしては、Passport::tokensCan
というメソッドを提供することで、AccessTokenを引数として受け取った時に、有効なscopeを保持しているかの認可チェックを行うことを考えているようです
その為、end pointとしての認可チェックが存在していないと思われます
スコープのチェックを参照ください
Route::get('/orders', function () {
// アクセストークンは"check-status"と"place-orders"、両スコープを持っている
})->middleware('scopes:check-status,place-orders');
Route::get('/orders', function () {
// アクセストークンは、"check-status"か"place-orders"、どちらかのスコープを持っている
})->middleware('scope:check-status,place-orders');
等の利用方法が推奨されています
チェックさせたいScopesは、以下のとおり AuthServiceProviderに記載することで設定可能となっています
use Laravel\Passport\Passport;
Passport::tokensCan([
'place-orders' => 'Place orders',
'check-status' => 'Check order status',
]);
Laraport(4.0)としての位置づけ
多分ですが、Laravel + passportは、Laravelの機能に準じた形での機能提供をしていると思うので、認証基盤と認可基盤(リソースサーバ)の両方でPassportを利用するのが、本来の使い方なのではないでしょうか?
よって、以下のような構成をとるのが正しいのではないかと思われます
よって、認証サーバ としては、下記のような設定を行い、artisan commandで鍵やclientidの生成を行う必要があると思っています。
--- App\User
<?php
namespace App;
use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
}
--- AuthServiceProvider
<?php
namespace App\Providers;
use Laravel\Passport\Passport;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* アプリケーションのポリシーのマップ
*
* @var array
*/
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
];
/**
* 全認証/認可サービスの登録
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Passport::routes();
Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(30));
}
}
認可機能を含むリソースサーバ(会員API) としては、下記のような設定を行い、認証サーバと同じDBを見ながらも、認可の為のscopeやTokenGuardを設定するのではないでしょうか?
Laravelで同時に二つのスキーマ又はDBを見させるのは面倒のような気がしますねー
認証サーバと、認可サーバと、リソースサーバを同梱させちゃうのが良いのかしら?
---config/auth.php
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],
---AuthServiceProvider::boot
/**
* 全認証/認可サービスの登録
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Passport::routes();
Passport::tokensCan([
'place-orders' => 'Place orders',
'check-status' => 'Check order status',
]);
}
}
-- routing
Route::get('/orders', function () {
// アクセストークンは、"check-status"か"place-orders"、どちらかのスコープを持っている
})->middleware('scope:check-status,place-orders');
※上記のコーディングは検証しているわけではないので、API認証(passport)を参照しながら自身で構築してみてください
2. Laraport(4.0)の環境構築方法
基本的には、何度もリンクを出していますが Laravel 5.5 API認証(passport)に従って構築してください
構築後は、簡単なのは Owner Password Grantを postman や curl等でリクエストしてみてください
php artisan passport:client --password
上記のコマンドをうつと、client_idとclient_secretが生成されますので、それを利用してトークンリクエストを出してみてください
無事、access tokenが返れば成功です
言わずもがなですが、postmanの参考画面を載せているだけですので、client_idやclient_secretは、生成されたものを利用してください
username, passwordは、以下のコマンドをmysql 等のデータベースから事前に入れ込んでおいてください
use oauth2; ← 私が設定したDBは、oauth2 にしています
insert into users (name,email,password) values ("taylor@laravel.com","taylor@laravel.com","my-password");
artisan する際に、--password が無いと、Owner Password Grantは失敗するので気を付けてください
エンティティ内に owner password grantの対象化を判断するフラグを持っている為です
環境構築時のartisan コマンドについて
|-- Console
| |-- ClientCommand.php
| |-- InstallCommand.php
| |-- KeysCommand.php
構築時に利用するatrisanコマンドは以下を呼び出すことになっています
php artisan passport:migrate
php artisan passport:install
passport:migrateは、事前に用意された database上のスキーマーにoauthで利用するテーブルを構築します
※.env に接続先databaseや、スキーマー、username、passwordを記載することになっているので、事前の環境構築を行ってから実行してください
passport:installは、以下の3つのコマンドを実行しています
その為、慣れた方は、自身で個別にコマンドを実行することも可能です
php artisan passport:keys
php artisan passport:client --personal
php artisan passport:client --password
ソースは、vendor/laravel/passport/src/Console
配下に入っているので内容をご確認ください
ソース | コマンド | 内容 |
---|---|---|
InstallCommand.php | passport:install | 鍵情報と共にClientIDを生成する |
ClientCommand.php | passport:client | ClientIDを生成する --passwordを指定すれば、OwnerPasswordで無ければ、AuthCodeとして生成する |
KeysCommand.php | passport:keys | 鍵ファイルを作成する |
InstallCommand.php等は、$this->call コマンドで上に書いた3つのartisan コマンドを実行しています
追記:Auth Code用のclient_idについて
ちなみに、見てわかる通りphp artisan passport:client
は、誰も実行していません。
今のままだと、AuthCodeGrantでの認証は出来ないので、必要であれば、auth code用のコマンドを実行する必要があります
php artisan passport:client
--passwordや--personalを指定すると、1st party用のclient_idが生成されます。
auth codeは、3rd partyの為の扱いになるので、オプションを指定しないで3rd party向けに生成したclient_idを利用する必要があります。
追記2:鍵情報について
php artisan passport:keys
を実行することで、鍵情報が生成されます。これらは、access token等を暗号化する際に利用する公開鍵情報となります
storage配下に、以下のように作成されます
|-- app
|-- framework
|-- logs
| `-- laravel.log
|-- oauth-private.key
`-- oauth-public.key
その際に、鍵情報は4096で作成しています
$keys = $rsa->createKey(4096)
を編集すれば鍵長は変更することが可能です
とはいえ、Json形式で各種情報を格納しているので、短くなるものではありません。
よって、あまり短くするとセキュリティ上問題があるので注意してください
最小は、1024はOKですが、256は当たり前のようにダメでしたww
/**
* Execute the console command.
*
* @param \phpseclib\Crypt\RSA $rsa
* @return mixed
*/
public function handle(RSA $rsa)
{
$keys = $rsa->createKey(4096);
list($publicKey, $privateKey) = [
Passport::keyPath('oauth-public.key'),
Passport::keyPath('oauth-private.key'),
];
if ((file_exists($publicKey) || file_exists($privateKey)) && ! $this->option('force')) {
return $this->error('Encryption keys already exist. Use the --force option to overwrite them.');
}
file_put_contents($publicKey, array_get($keys, 'publickey'));
file_put_contents($privateKey, array_get($keys, 'privatekey'));
$this->info('Encryption keys generated successfully.');
}
3. 認証サーバとリソースサーバの構成について
本来、会員情報サーバは、会員情報を管理するリソースサーバなので、認証サーバとは別構成として作られることが望ましいです
大抵は以下の3種類で構築されることが多いと思われます
1) 会員情報サーバとの間をAPI経由とするパターン
会員情報サーバは本来リソースサーバなので、会員の有無チェックはID/PWを引数としたログインAPIを経由して、認証サーバからの資格チェックに答えることとする
2) 会員情報サーバの情報を認証サーバにコピーするパターン
会員情報サーバに新規会員が作成されたタイミングで、認証サーバのデータベースにも同じ会員情報を登録して、認証サーバ内で資格チェックを行う
実は、会員情報サーバのデータベースメンテナンス等を行う際でも、Read系のみのサービス無停止(いつでもログイン可能)等が出来るので意外とバカに出来ないが同期タイミングが難しいという問題もある
3) 会員情報サーバと認証サーバを同じ又はDBを共有するパターン(現行のLaraport(4.0))
認証サーバと会員サーバを同じサーバ又はDBを共有させて、認証サーバが持つusersテーブルで会員の資格チェックを行う
4. 認証サーバとリソースサーバの分離させる方法
認証サーバとリソースサーバの分離とは?
上記で書いた通り、Laraportは会員情報と認証サーバを同じ環境で提供する構成としている
その為、リソースサーバとして別だししたい場合は、Passportのソースコードに手を入れる必要がある
その際の、サーバ構成は以下のとおりとなる。
ポイントとしては、以下の2点を実現する必要があるです
- 認証サーバでのログインチェック時に、会員情報サーバに資格チェックを行うためのWeb API呼び出しを行う
- リソースサーバ(point)の情報を取得する前に、トークンイントロスペクションを行うことで access tokenのチェックを行い会員IDを取得する
認証サーバと会員情報サーバ(リソースサーバ)等を構築する方法
Laraportを改造して、構築するためには、以下の箇所を修正することで対応することが出来ます
1. UserRepository内ので、資格情報のチェックの修正
vendor
|--laravel
| |--passport/
| | |--src
| | | |-- ApiTokenCookieFactory.php
| | | |-- AuthCode.php
| | | |-- Bridge
| | | | |-- AccessToken.php
| | | | |-- AccessTokenRepository.php
| | | | |-- <snip>
| | | | |-- User.php
| | | | `-- UserRepository.php
内にある、UserRepository.phpを確認してください
getUserEntityByUserCredentials内で、usersデータベースを検索してアカウントの有無をチェックしていますが、
ここで、会員情報チェック用のWebAPIを呼び出して、最終行に会員IDを引数として渡してあげると、access tokenの sub に対して会員情報を設定することが出来ます
{
$http = new GuzzleHttp\Client;
$response = $http->post('http://your-app.com/login', [
'form_params' => [
'username' => $username,
'password' => $password,
],
]);
credentials = json_decode((string) $response->getBody(), true);
return new User($credentials['memberId']);
}
会員APIサーバをリソースサーバとして扱う場合は、ClientID/CliendSecretを渡すか ClientConfidentialsGrant等でサーバ認証を行う様にする
{
$http = new GuzzleHttp\Client;
$response = $http->post('http://your-app.com/login', [
'form_params' => [
'clientId' => $clientId,
'clientSecret' => $clientSecret,
'username' => $username,
'password' => $password,
],
]);
credentials = json_decode((string) $response->getBody(), true);
return new User($credentials['memberId']);
}
2. token introspect時の、トークンの展開時のチェック方法
token introspectは、Laravel + Passportでは、ipunkt/laravel-oauth-introspectionを利用することで提供することが出来ます
ディレクトリを展開すると以下のような形となります
# tree vendor/ipunkt/
ipunkt/
|-- laravel-oauth-introspection
| |-- LICENSE
| |-- README.md
| |-- composer.json
| |-- routes
| | `-- web.php
| `-- src
| |-- Http
| | `-- Controllers
| | `-- IntrospectionController.php
| `-- Providers
| |-- OAuthIntrospectionServiceProvider.php
| `-- RouteProvider.php
対象のソースを見ると、以下のとおりです
赤枠で囲われた箇所が、usersテーブルを参照しているところです
併せて、usernameの結果として、usersで取得したemail を設定しています
usernameは、RFC的にはOPTIONなので無視することとすれば、
今回の、access tokenを展開する際に、usernameを設定しない代わりに、usersテーブルを利用しないで introspectionの機能を提供できるようになります。
5. Revokeについて
Laravel + Passportでは、各tokenのテーブルに対して deleteする機能は提供していません
すべてのトークンに対して、無効化させる際は、revokeフラグを1にするという処理を行っています
よって、revokeフラグを見て、1日1回程度で各テーブルを掃除するバッチを作らないと、ごみが溜まっていきますので注意してください
A. Appendix テーブルやend-point一覧
table list
mysql> show tables;
Tables_in_database | component |
---|---|
oauth_access_tokens | passport |
oauth_auth_codes | passport |
oauth_clients | passport |
oauth_personal_access_clients | passport |
oauth_refresh_tokens | passport |
users | oauth2-server-laravel |
migrations | oauth2-server-laravel? |
password_resets | oauth2-server-laravel? |
each tables structure
mysql> desc oauth_clients;
+------------------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| user_id | int(11) | YES | MUL | NULL | |
| name | varchar(255) | NO | | NULL | |
| secret | varchar(100) | NO | | NULL | |
| redirect | text | NO | | NULL | |
| personal_access_client | tinyint(1) | NO | | NULL | |
| password_client | tinyint(1) | NO | | NULL | |
| revoked | tinyint(1) | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+------------------------+------------------+------+-----+---------+----------------+
mysql> desc oauth_auth_codes;
+------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| id | varchar(100) | NO | PRI | NULL | |
| user_id | int(11) | NO | | NULL | |
| client_id | int(11) | NO | | NULL | |
| scopes | text | YES | | NULL | |
| revoked | tinyint(1) | NO | | NULL | |
| expires_at | datetime | YES | | NULL | |
+------------+--------------+------+-----+---------+-------+
mysql> desc oauth_access_tokens;
+------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| id | varchar(100) | NO | PRI | NULL | |
| user_id | int(11) | YES | MUL | NULL | |
| client_id | int(11) | NO | | NULL | |
| name | varchar(255) | YES | | NULL | |
| scopes | text | YES | | NULL | |
| revoked | tinyint(1) | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
| expires_at | datetime | YES | | NULL | |
+------------+--------------+------+-----+---------+-------+
mysql> desc oauth_personal_access_clients;
+------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| client_id | int(11) | NO | MUL | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+------------+------------------+------+-----+---------+----------------+
mysql> desc oauth_refresh_tokens;
+-----------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+--------------+------+-----+---------+-------+
| id | varchar(100) | NO | PRI | NULL | |
| access_token_id | varchar(100) | NO | MUL | NULL | |
| revoked | tinyint(1) | NO | | NULL | |
| expires_at | datetime | YES | | NULL | |
+-----------------+--------------+------+-----+---------+-------+
mysql> desc users;
+----------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | | NULL | |
| email | varchar(255) | NO | UNI | NULL | |
| password | varchar(255) | NO | | NULL | |
| remember_token | varchar(100) | YES | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+----------------+------------------+------+-----+---------+----------------+
mysql> desc migrations;
+-----------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| migration | varchar(255) | NO | | NULL | |
| batch | int(11) | NO | | NULL | |
+-----------+------------------+------+-----+---------+----------------+
mysql> desc password_resets;
+------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| email | varchar(255) | NO | MUL | NULL | |
| token | varchar(255) | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
+------------+--------------+------+-----+---------+-------+
Routing
[root@888dc9550825 laravel55]# php artisan route:list
+--------+----------+-----------------------------------------+------+---------------------------------------------------------------------------------------------+--------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+----------+-----------------------------------------+------+---------------------------------------------------------------------------------------------+--------------+
| | POST | oauth/authorize | | \Laravel\Passport\Http\Controllers\ApproveAuthorizationController@approve | web,auth |
| | DELETE | oauth/authorize | | \Laravel\Passport\Http\Controllers\DenyAuthorizationController@deny | web,auth |
| | GET|HEAD | oauth/authorize | | \Laravel\Passport\Http\Controllers\AuthorizationController@authorize | web,auth |
| | GET|HEAD | oauth/clients | | \Laravel\Passport\Http\Controllers\ClientController@forUser | web,auth |
| | POST | oauth/clients | | \Laravel\Passport\Http\Controllers\ClientController@store | web,auth |
| | PUT | oauth/clients/{client_id} | | \Laravel\Passport\Http\Controllers\ClientController@update | web,auth |
| | DELETE | oauth/clients/{client_id} | | \Laravel\Passport\Http\Controllers\ClientController@destroy | web,auth |
| | POST | oauth/introspect | | \Ipunkt\Laravel\OAuthIntrospection\Http\Controllers\IntrospectionController@introspectToken | |
| | GET|HEAD | oauth/personal-access-tokens | | \Laravel\Passport\Http\Controllers\PersonalAccessTokenController@forUser | web,auth |
| | POST | oauth/personal-access-tokens | | \Laravel\Passport\Http\Controllers\PersonalAccessTokenController@store | web,auth |
| | DELETE | oauth/personal-access-tokens/{token_id} | | \Laravel\Passport\Http\Controllers\PersonalAccessTokenController@destroy | web,auth |
| | GET|HEAD | oauth/scopes | | \Laravel\Passport\Http\Controllers\ScopeController@all | web,auth |
| | POST | oauth/token | | \Laravel\Passport\Http\Controllers\AccessTokenController@issueToken | throttle |
| | POST | oauth/token/refresh | | \Laravel\Passport\Http\Controllers\TransientTokenController@refresh | web,auth |
| | GET|HEAD | oauth/tokens | | \Laravel\Passport\Http\Controllers\AuthorizedAccessTokenController@forUser | web,auth |
| | DELETE | oauth/tokens/{token_id} | | \Laravel\Passport\Http\Controllers\AuthorizedAccessTokenController@destroy | web,auth |
+--------+----------+-----------------------------------------+------+---------------------------------------------------------------------------------------------+--------------+
/oauth/tokens や、/oauth/clients 等は、実質要らないと思います
oauth/introspectは、外部コンポーネントを入れると使えるようになります。
Passport.phpによるpublic methods
method name | detail | note |
---|---|---|
enableImplicitGrant | implicit grant typeを有効化 | |
routes($callback, $options) | Passportのルートをコントローラにバインド | |
revokeOtherTokens | token生成時に既存tokenを全てrevokeするよう指示(現状実装無し) | @deprecated https://readouble.com/laravel/5.5/ja/passport.html#events で実施 |
pruneRevokedTokens | revokeされたトークンを抹消するよう指示(現状実装無し) | @deprecated https://readouble.com/laravel/5.5/ja/passport.html#events で実施 |
personalAccessClient($clientId) | personal access_tokenを発行できるclientIdをセット | |
scopeIds | 定義されている全てのscope idを取得 | |
hasScope($id) | 定義済scopeかチェック | |
scopes | アプリケーションで定義済のScopeを取得 | |
scopesFor(array $ids) | 渡されたidに紐づく全てのScopeを取得 | |
tokensCan(array $scopes) | 渡されたscopesをセット | |
tokensExpireIn | 渡されたaccess_token有効期限日をセット、なければ残り時間を返却(そもそもなければ残り1年で返却) | |
refreshTokensExpireIn | 渡されたrefresh_token有効期限日をセット、なければ残り時間を返却(そもそもなければ残り1年で返却) | |
cookie($cookie) | API token cookieが渡されればセット、引数無しならAPI token cookieを返却 | |
actingAs | 現在ログインしているユーザの判定判別 | |
loadKeysFrom | 暗号化キーの保管場所をセット | |
keyPath | 暗号化キーの保管場所を返却 https://qiita.com/kawax/items/59fde47056816cec52ec | |
ignoreMigrations | Migration情報として登録させない |
password grantのフロー
B. Appendix
バグ対応
- refresh tokenを毎回再生成させない方法
- 日付をみて、revokeさせるコードが何処にもないので、refresh token grantを実施しない場合は、それに該当しないrefresh tokenは全てデータベース内に滞留することになる件の対応
- 別途、ゴミ掃除のバッチを作って、revokeフラグを立てる処理と、revokeフラグが立っているレコードをお掃除する処理を用意する必要がある
- 実質は、カスタムプロバイダーとして登録させてあげるのが良いが、ここでは、対象となるソースを書き換える前提とする
- refresh tokenを使ってaccess tokenを生成するということは、ここで会員の退会チェック等も必要になるので、別途会員DBまたは会員APIで存在チェックを行うのがより良い実装だと思われる
- 以下のサンプルは実装修正として記載しているが、本来はAPI呼び出し系なので、Custom Providerとして提供するのが良いと思う
-- いずれ、はじめくんが提供してくれるものと思っているww
/**
* {@inheritdoc}
*/
public function respondToAccessTokenRequest(
ServerRequestInterface $request,
ResponseTypeInterface $responseType,
\DateInterval $accessTokenTTL
) {
// Validate request
$client = $this->validateClient($request);
$oldRefreshToken = $this->validateOldRefreshToken($request, $client->getIdentifier());
$scopes = $this->validateScopes($this->getRequestParameter(
'scope',
$request,
implode(self::SCOPE_DELIMITER_STRING, $oldRefreshToken['scopes']))
);
// The OAuth spec says that a refreshed access token can have the original scopes or fewer so ensure
// the request doesn't include any new scopes
foreach ($scopes as $scope) {
if (in_array($scope->getIdentifier(), $oldRefreshToken['scopes']) === false) {
throw OAuthServerException::invalidScope($scope->getIdentifier());
}
}
// Expire old tokens
$this->accessTokenRepository->revokeAccessToken($oldRefreshToken['access_token_id']);
// @may PR: refresh tokenのrevoked処理をコメントアウトする
// $this->refreshTokenRepository->revokeRefreshToken($oldRefreshToken['refresh_token_id']);
// @TODO ここに会員存在チェックを行う API 等を挿入するのが良いと思われる
// Issue and persist new tokens
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $oldRefreshToken['user_id'], $scopes);
// @may PR: refresh tokenのrevoked処理をコメントアウトする
// $refreshToken = $this->issueRefreshToken($accessToken);
$this->refreshTokenRepository->switchNewAccessToken(
$oldRefreshToken['refresh_token_id'],
$accessToken->getIdentifier());
// Inject tokens into response
$responseType->setAccessToken($accessToken);
// refresh tokenを返答させないようにする
// $responseType->setRefreshToken($oldRefreshToken['refresh_token_id']);
return $responseType;
}
protected function validateOldRefreshToken(ServerRequestInterface $request, $clientId)
{
$encryptedRefreshToken = $this->getRequestParameter('refresh_token', $request);
if (is_null($encryptedRefreshToken)) {
throw OAuthServerException::invalidRequest('refresh_token');
}
// Validate refresh token
try {
$refreshToken = $this->decrypt($encryptedRefreshToken);
} catch (\Exception $e) {
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token');
}
$refreshTokenData = json_decode($refreshToken, true);
if ($refreshTokenData['client_id'] !== $clientId) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_CLIENT_FAILED, $request));
throw OAuthServerException::invalidRefreshToken('Token is not linked to client');
}
if ($refreshTokenData['expire_time'] < time()) {
//- @may PR: refresh tokenがexpireならば、revokedのフラグを立てる必要がある
$this->refreshTokenRepository->revokeRefreshToken($refreshTokenData['refresh_token_id']);
throw OAuthServerException::invalidRefreshToken('Token has expired');
}
if ($this->refreshTokenRepository->isRefreshTokenRevoked($refreshTokenData['refresh_token_id']) === true) {
throw OAuthServerException::invalidRefreshToken('Token has been revoked');
}
return $refreshTokenData;
}
/**
* {@inheritdoc}
*/
public function switchNewAccessToken($refreshTokenId, $accessTokenId) {
$this->database->table('oauth_refresh_tokens')
->where('id', $refreshTokenId)->update(['access_token_id' => $accessTokenId]);
}