Laravel 5.3 ではOAuth2プロバイダを提供するための公式パッケージLaravel/passportが登場しました。
このパッケージを使えば難しい設定なしにすぐにOAuth2プロバイダをアプリケーションに構築することが可能です。では、やってみましょう。
基本的な手順は公式ドキュメントにあるとおりです。(日本語版は現時点で未訳)→翻訳されました。
Laravel Passportのドキュメント、独自用語があるので、ソースを確認しながら翻訳。こりゃ時間がかかる。 #例の愚痴です
— Hirohisa Kawase (@HiroKws) September 25, 2016
(お疲れ様です)
今回はアプリケーションの作成から追っていきます。
もちろんLaravel 5.3の動作要件であるPHP5.6.4+が必要です。
(手元では5.6.24/7.0.10でテストしています。)
Windowsの場合はmingw等を使うか、./artisan
をphp artisan
に読み替えてください。
Laravelの初期セットアップ
まず、アプリケーションの作成。今回はcomposer create-project
を使いました。
1 2 |
composer create-project laravel/laravel passport-sample cd passport-sample |
.env
を適当に編集し、DBの設定を済ませたらここで一旦マイグレーションをしてユーザーテーブルを作っておきましょう。
1 |
./artisan migrate |
また、ユーザー作成や後述する認可で必要になるのでログイン画面等の自動生成もやっておきましょう。
1 |
./artisan make:auth |
ここまでできたら一旦サーバーを起動し、ブラウザでアクセスしてみましょう。
1 |
./artisan serve |
表示されたURLを開き、Welcome画面を表示することが出来たでしょうか。できたらそのままユーザー登録まで済ませてしまいましょう。
(既存の起動しているものとポートが競合した場合は--port
オプションで他のポートを指定しましょう)
Passportをインスインストール
composerでPassportをプロジェクトに追加します。
1 |
composer require laravel/passport |
そして、config/app.phpのproviders
(169行目辺り)に Laravel\Passport\PassportServiceProvider::class,
を追加。
passportが用意しているトークン管理用DBを作成するため再度マイグレーションを行います。
1 |
./artisan migrate |
DBにはoauth_
をプレフィックスとする5つのテーブルが作成されます。
アクセストークン生成用のキーペアとシステムクライアント生成も行いましょう。ここで言うクライアントとはTwitterで言うTweetBotやtwiccaのようなアプリケーションの持つものです。
1 |
./artisan passport:install |
ここでstorage
にキーペアが生成されますが、これはセキュリティ的には公開リポジトリに置くべきでは無い気がします。特に秘密鍵。
そして、HasApiTokens
トレイトをユーザーModel に設定しておきましょう。トークンやスコープにアクセスするメソッドが追加されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
diff --git a/app/User.php b/app/User.php index bfd96a6..0638c66 100644 --- a/app/User.php +++ b/app/User.php @@ -2,12 +2,13 @@ namespace App; +use Laravel\Passport\HasApiTokens; use Illuminate\Notifications\Notifiable; use Illuminate\Foundation\Auth\User as Authenticatable; class User extends Authenticatable { - use Notifiable; + use HasApiTokens, Notifiable; /** * The attributes that are mass assignable. |
そして、Passportの用意しているOAuthメソッドにアクセスするためのルーティングを`app/Providers/AuthServiceProvider.phpに追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 9784b1a..fd77876 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use Laravel\Passport\Passport; use Illuminate\Support\Facades\Gate; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; @@ -25,6 +26,7 @@ class AuthServiceProvider extends ServiceProvider { $this->registerPolicies(); + Passport::routes(); // } } |
ルーティングを見てみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
$ ./artisan route:list +--------+----------+-----------------------------------------+-------+----------------------------------------------------------------------------+--------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+----------+-----------------------------------------+-------+----------------------------------------------------------------------------+--------------+ | | GET|HEAD | / | | Closure | web | | | GET|HEAD | api/user | | Closure | api,auth:api | | | GET|HEAD | home | | App\Http\Controllers\HomeController@index | web,auth | | | GET|HEAD | login | login | App\Http\Controllers\Auth\LoginController@showLoginForm | web,guest | | | POST | login | | App\Http\Controllers\Auth\LoginController@login | web,guest | | | POST | logout | | App\Http\Controllers\Auth\LoginController@logout | web | | | POST | oauth/authorize | | \Laravel\Passport\Http\Controllers\ApproveAuthorizationController@approve | web,auth | | | GET|HEAD | oauth/authorize | | \Laravel\Passport\Http\Controllers\AuthorizationController@authorize | web,auth | | | DELETE | oauth/authorize | | \Laravel\Passport\Http\Controllers\DenyAuthorizationController@deny | web,auth | | | POST | oauth/clients | | \Laravel\Passport\Http\Controllers\ClientController@store | web,auth | | | GET|HEAD | oauth/clients | | \Laravel\Passport\Http\Controllers\ClientController@forUser | 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/personal-access-tokens | | \Laravel\Passport\Http\Controllers\PersonalAccessTokenController@store | web,auth | | | GET|HEAD | oauth/personal-access-tokens | | \Laravel\Passport\Http\Controllers\PersonalAccessTokenController@forUser | 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 | | | | 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 | | | POST | password/email | | App\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail | web,guest | | | POST | password/reset | | App\Http\Controllers\Auth\ResetPasswordController@reset | web,guest | | | GET|HEAD | password/reset | | App\Http\Controllers\Auth\ForgotPasswordController@showLinkRequestForm | web,guest | | | GET|HEAD | password/reset/{token} | | App\Http\Controllers\Auth\ResetPasswordController@showResetForm | web,guest | | | POST | register | | App\Http\Controllers\Auth\RegisterController@register | web,guest | | | GET|HEAD | register | | App\Http\Controllers\Auth\RegisterController@showRegistrationForm | web,guest | +--------+----------+-----------------------------------------+-------+----------------------------------------------------------------------------+--------------+ |
アクセストークンやクライアントを管理するAPIが追加されています。
最後に、config/auth.php
の45行目辺り、apiのGuardドライバをtoken
からpassport
に差し替えておきます。
これでPassportの導入は完了です。
ここまで行ったサンプルをGitHubに用意しました。
OAuth2 でフロントからAPIを叩いてみる
では、APIが使えるか試してみましょう。
デフォルトでLaravelは認証ユーザーを返すAPIを/api/user
に用意しています。
1 2 3 |
Route::get('/user', function (Request $request) { return $request->user(); })->middleware('auth:api'); |
普通にこのAPIにアクセスしようとすると、認証していないので401となり、ログインページに飛ばされるか、{"error":"Unauthenticated."}
が返されるでしょう。(この挙動はapp/Exceptions/Handler.php
で自由に変更可能です)
なお、JSONが返されるのはLaravel5.3のデフォルトではヘッダーにX-Requested-With: XMLHttpRequest
がある、またはAccept
ヘッダーでjsonが指定されている場合です。(fetchにネイティブ対応しているブラウザではそれを使用した場合明示的にAccept
を指定してやらないと前者の対象にはならずにリダイレクトが発生し200になります。宇宙。)
では、トークンを取得してAPIが叩けるようになるのを目指しましょう。いくつかの方法を紹介します。(例ではいずれもJSONでPOSTを投げていますがもちろんformdataでも構いません。)
デフォルトで上で予めクライアントを生成しているのでoauth_clients
テーブルにあるclient_idとsecretを控えておきましょう。(もうちょいいい確認方法はないのか)1がパーソナルアクセストークン用、2がパスワードログイン用。詳しくは後述。
パスワードを用いてトークンを取得する
割と一般的な話かと思います。例えばアプリケーション自体(=1st party) をSPAにする場合、ユーザー名+パスワードでログインさせることになるでしょう。この場合なんかに使えます。
トークン取得のためのリクエスト回数が1回で済むのも特徴です。
必要な情報は以下のとおり
- 上で確認したclient_id, secretのペア
- ユーザー名
- パスワード
です。
/oauth/token
にこんな感じのPOSTをします。
1 2 3 4 5 6 7 8 |
{ "client_id": 2, "client_secret": "OkjBjNF4rgqsXIBqVvP1NXFbgjvY4C62mnDlmCBv", "username": "hina@hinaloe.net", "password": "your_password", "grant_type": "password", "scope": "" } |
正しくリクエストできていればアクセストークンとリフレッシュトークンが返されます。リフレッシュトークンはその名の通り、トークン更新用のトークンです。いずれにも有効期限がありますが、デフォルトでは実質無期限です。 Passport::tokensExpireIn
,Passport::refreshTokensExpireIn
を設定するようにしましょう。
この方法の懸念点としては、secretを使用する必要があるので公開ログインに使えるものなのか?といった問題があります。また、同じアカウントで同じクライアントを使用しログインするとこのアクセストークンは無効化されます。→ Passport v1.0.5よりこのタイミングでの失効は廃止されました。(もちろんオーバーライドすれば復活させられます)(トークンのリフレッシュが必要になります。)
code を用いてログインする
ブラウザでのログイン情報を利用し、外部アプリケーション等へアプリケーションアクセスを提供します。TwitterやFacebook、GitHubなんかのサードパーティアプリ認証と同様です。
アプリ内で認可画面が表示され、サードパーティ製アプリへリダイレクトされます。
passwordログイン用のクライアントはここでは使用できないようなので新規にクライアントを作成して利用します。
クライアントの作成
artisanコマンドから
1 |
./artisan passport:client |
でクライアントのオーナーID,クライアント名、コールバックURLを対話式に入力し、クライアントを作成します。idとシークレットが表示されるので控えておきます。
APIから
1 2 3 4 |
{ "name": "Client Name", "redirect": "http://example.com/callback" } |
のようなデータを/oauth/clientsに投げます。(要認証)
なお、ログインセッションを利用してAPIを叩くため、デフォルトではcsrfTokenヘッダーも送信する必要があるので注意。
認可ページを表示
認可ページは実質的に固定リンクとなります。
http://localhost:8050/oauth/authorize?response_type=code&client_id=3&redirect_uri=http://localhost/auth/callback
みたいな。
アクセスするとこのような認可画面が表示されるので承認すると承認するとGETクエリにcodeをつけて指定URLにリダイレクトします。
CSRF対策にstate パラメータを利用することが推奨されます。もちろん対応しています。
リダイレクト先から認証
リダイレクト先からは/oauth/token
に
1 2 3 4 5 6 7 |
{ "grant_type": "authorization_code", "client_id": "3", "client_secret": "s4ysEJEWwp9Xk8G6jZCai9FAYUTogpPJgMUFfWdb", "redirect_uri": "http://localhost/auth/callback", "code": "(受け取ったcode)" } |
のようなリクエストを投げると、同様にトークンのペアが返されます。
もちろんOAuth2に準拠しているので例えばこの方式、サードパーティ製のAPI叩くツールの利用にも活躍します。
例えばPostmanの場合………
そのまま簡単に認証が出来ました。
APIの開発が捗る……
パーソナルアクセストークン
いわゆるQiitaやGitHubなんかで利用できる「個人用アクセストークン」です。3rd partyアプリ内での認証が不要で、自分で無効化しないかぎり永続的に利用可能なアクセストークンをユーザー誰でも発行できます。
APIを利用して発行する場合、webで認証済みのセッションが利用可能である場合のみ、それを利用して発行、確認、削除することが出来ます。
personal_access_client=1になっているクライアントが必要ですが、passport:install
の際に作成されているはずなので特別な操作は必要ないはずです。
フロントで、/oauth/personal-access-tokens
に
1 2 3 4 |
{ "name":"test token", "scope":"" } |
のようなデータをpostするとアクセストークンが発行されます。
フロントからPOSTする際は上記同様、CSRFトークンをヘッダーに含める必要があるのでお忘れなく。
なお、このトークンはtokensExpireIn
を無視し、実質無期限なものになります。
発行済のトークン一覧は GET /oauth/personal-access-tokens
、削除はdelete /oauth/personal-access-tokens/:id
で出来ます。
(いずれもセッションを利用)
取得したトークンを用いてAPIを叩く
ここまで何れかの方法でアクセストークンが取得できれば、これを用いてAPIを叩くことが出来るはずです。
認証情報はAuthorizationヘッダーを用いて渡します。
'Authorization': 'Bearer '+token
をヘッダーに追加するだけ。
どうですか?出来ましたか?
トークンの更新
トークンが期限切れしたら、トークンを更新する必要があります。期限切れには時間経過以外の要因(多重ログインなど)もあるため、時間切れのみを更新のトリガーにしてはいけません。APIを叩いた際に401が返ってきたら、それは更新の合図です。(その更新にも失敗したら、再認証させる必要があるでしょう。)
1 2 3 4 5 6 7 |
{ "client_id": 2, "client_secret": "OkjBjNF4rgqsXIBqVvP1NXFbgjvY4C62mnDlmCBv", "grant_type": "refresh_token", "scope": "", "refresh_token": "(アクセストークン取得時に取得したrefresh token)" } |
このようなリクエストを、認証時同様に/oauth/tokenにPOSTします。成功すると新しいアクセストークンとリフレッシュトークンのペアが返されます。(もちろんリフレッシュトークンの再利用は出来ません)
scope
OAuthではトークンにアクセス範囲の制限をつけることが可能です。
Twitterではread,read and write, +DM, +emailの4種類があり、もっと細かく分けろって散々言われてます。FBやGitHub, G+では細かく分かれてますね。
今回は利用しませんでしたが、もちろん認証時に、scopeを投げることでこれが利用可能です。 scopes:check-status,place-orders
のようなミドルウェアの指定や、User
インスタンスから生えてるメソッドを使った$request->user()->tokenCan('place-orders')
のようなメソッドを使ったりしてこれを分岐できます。なお、scope:'*'
を指定した場合は全てのスコープを持つと解釈されます。
まとめ
一連の操作はJavaScriptのajax/fetchだけで完結することが出来るため、Laravelを使ったSPAをつくるのにも便利かと思います。(小並感)
ただ、デフォルトで少しトークン発行周りが甘すぎるとこが或るんじゃないか……?
まぁAPI一般公開するつもりがないものにOAuth2導入するなんて考えがそもそもおかしいのかもしれないけど。
ところでこれ書くのにテストする時パラメータ間違えたりして何回リクエスト失敗したんだろう……
Passportで利用する認証方法を絞る方法も書きました。 Laravel の Passport で利用できる認証方法を制限する – Qiita
./artisan migrateを実行すると
DBにはoauth_をプレフィックスとする5つのテーブルが作成されます。とありますが、
私の環境(centos7,laravel5.4)ではなぜかテーブル作成のエラーが表示されてしまいました。
結果的に、config/database.phpにある、文字コードをutf8_general_ciに変更すると問題なくmigrateされました。
パスワードを用いてトークンを取得するの項で、POSTするurlが書かれておらず、api/userに投げてしまいました。
oauth/tokenにpostすると書き加えると良いかと思います。
ご指摘有難うございます。
すっかり見落としてましたが確かに漏れてますね……
とりあえず書き足しておきました。(ついでに読み直して引っかかったとことかも修正……)
上のPostmanを使って(grant-type Authorization code)なんですが
could not complete oauth2 login というエラーが出てしまいます。
アドバイスを頂けたらありがたいです。