Laravel 5.3 で Passportを試す

Laravel 5.3 ではOAuth2プロバイダを提供するための公式パッケージLaravel/passportが登場しました。

このパッケージを使えば難しい設定なしにすぐにOAuth2プロバイダをアプリケーションに構築することが可能です。では、やってみましょう。

基本的な手順は公式ドキュメントにあるとおりです。(日本語版は現時点で未訳)→翻訳されました。

(お疲れ様です)

今回はアプリケーションの作成から追っていきます。

もちろんLaravel 5.3の動作要件であるPHP5.6.4+が必要です。
(手元では5.6.24/7.0.10でテストしています。)
Windowsの場合はmingw等を使うか、./artisanphp artisanに読み替えてください。


Laravelの初期セットアップ

まず、アプリケーションの作成。今回はcomposer create-projectを使いました。

.envを適当に編集し、DBの設定を済ませたらここで一旦マイグレーションをしてユーザーテーブルを作っておきましょう。

また、ユーザー作成や後述する認可で必要になるのでログイン画面等の自動生成もやっておきましょう。

ここまでできたら一旦サーバーを起動し、ブラウザでアクセスしてみましょう。

表示されたURLを開き、Welcome画面を表示することが出来たでしょうか。できたらそのままユーザー登録まで済ませてしまいましょう。

(既存の起動しているものとポートが競合した場合は--portオプションで他のポートを指定しましょう)

ユーザー登録

とうろくしてこの画面が出ればok.
登録してこの画面が出ればok.

Passportをインスインストール

composerでPassportをプロジェクトに追加します。

そして、config/app.phpのproviders (169行目辺り)に Laravel\Passport\PassportServiceProvider::class, を追加。

passportが用意しているトークン管理用DBを作成するため再度マイグレーションを行います。

DBにはoauth_をプレフィックスとする5つのテーブルが作成されます。

アクセストークン生成用のキーペアとシステムクライアント生成も行いましょう。ここで言うクライアントとはTwitterで言うTweetBotやtwiccaのようなアプリケーションの持つものです。

ここでstorageにキーペアが生成されますが、これはセキュリティ的には公開リポジトリに置くべきでは無い気がします。特に秘密鍵。

そして、HasApiTokens トレイトをユーザーModel に設定しておきましょう。トークンやスコープにアクセスするメソッドが追加されます。

そして、Passportの用意しているOAuthメソッドにアクセスするためのルーティングを`app/Providers/AuthServiceProvider.phpに追加。

ルーティングを見てみましょう。

アクセストークンやクライアントを管理するAPIが追加されています。

最後に、config/auth.phpの45行目辺り、apiのGuardドライバをtokenからpassportに差し替えておきます。

これでPassportの導入は完了です。

ここまで行ったサンプルをGitHubに用意しました。

hinaloe/passport-sample

OAuth2 でフロントからAPIを叩いてみる

では、APIが使えるか試してみましょう。

デフォルトでLaravelは認証ユーザーを返すAPIを/api/userに用意しています。

普通にこのAPIにアクセスしようとすると、認証していないので401となり、ログインページに飛ばされるか、{"error":"Unauthenticated."}が返されるでしょう。(この挙動はapp/Exceptions/Handler.phpで自由に変更可能です)

なお、JSONが返されるのはLaravel5.3のデフォルトではヘッダーにX-Requested-With: XMLHttpRequestがある、またはAcceptヘッダーでjsonが指定されている場合です。(fetchにネイティブ対応しているブラウザではそれを使用した場合明示的にAcceptを指定してやらないと前者の対象にはならずにリダイレクトが発生し200になります。宇宙。)

ChromeでjQueryで叩いてみた
ChromeでjQueryで叩いてみた
同様の挙動を想定してfetchしてみた
同様の挙動を想定してfetchしてみた
明示的に指定すれば大丈夫
明示的に指定すれば大丈夫

では、トークンを取得してAPIが叩けるようになるのを目指しましょう。いくつかの方法を紹介します。(例ではいずれもJSONでPOSTを投げていますがもちろんformdataでも構いません。)

デフォルトで上で予めクライアントを生成しているのでoauth_clientsテーブルにあるclient_idとsecretを控えておきましょう。(もうちょいいい確認方法はないのか)1がパーソナルアクセストークン用、2がパスワードログイン用。詳しくは後述。

パスワードを用いてトークンを取得する

割と一般的な話かと思います。例えばアプリケーション自体(=1st party) をSPAにする場合、ユーザー名+パスワードでログインさせることになるでしょう。この場合なんかに使えます。

トークン取得のためのリクエスト回数が1回で済むのも特徴です。

必要な情報は以下のとおり

  • 上で確認したclient_id, secretのペア
  • ユーザー名
  • パスワード

です。

/oauth/tokenにこんな感じのPOSTをします。

正しくリクエストできていればアクセストークンとリフレッシュトークンが返されます。リフレッシュトークンはその名の通り、トークン更新用のトークンです。いずれにも有効期限がありますが、デフォルトでは実質無期限です。 Passport::tokensExpireIn,Passport::refreshTokensExpireInを設定するようにしましょう。

この方法の懸念点としては、secretを使用する必要があるので公開ログインに使えるものなのか?といった問題があります。また、同じアカウントで同じクライアントを使用しログインするとこのアクセストークンは無効化されます。→ Passport v1.0.5よりこのタイミングでの失効は廃止されました。(もちろんオーバーライドすれば復活させられます)(トークンのリフレッシュが必要になります。)

code を用いてログインする

ブラウザでのログイン情報を利用し、外部アプリケーション等へアプリケーションアクセスを提供します。TwitterやFacebook、GitHubなんかのサードパーティアプリ認証と同様です。

アプリ内で認可画面が表示され、サードパーティ製アプリへリダイレクトされます。

passwordログイン用のクライアントはここでは使用できないようなので新規にクライアントを作成して利用します。

クライアントの作成

artisanコマンドから

でクライアントのオーナーID,クライアント名、コールバックURLを対話式に入力し、クライアントを作成します。idとシークレットが表示されるので控えておきます。

APIから

のようなデータを/oauth/clientsに投げます。(要認証)

なお、ログインセッションを利用してAPIを叩くため、デフォルトではcsrfTokenヘッダーも送信する必要があるので注意。

/oauth/clients

認可ページを表示

認可ページは実質的に固定リンクとなります。

http://localhost:8050/oauth/authorize?response_type=code&client_id=3&redirect_uri=http://localhost/auth/callback

みたいな。

アクセスするとこのような認可画面が表示されるので承認すると承認するとGETクエリにcodeをつけて指定URLにリダイレクトします。

CSRF対策にstate パラメータを利用することが推奨されます。もちろん対応しています。

リダイレクト先から認証

リダイレクト先からは/oauth/token

のようなリクエストを投げると、同様にトークンのペアが返されます。

もちろんOAuth2に準拠しているので例えばこの方式、サードパーティ製のAPI叩くツールの利用にも活躍します。

例えばPostmanの場合………

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-09-15-7-38-51 %e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-09-15-7-39-01 %e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-09-15-7-39-51 %e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-09-15-7-40-22 %e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-09-15-7-41-04

そのまま簡単に認証が出来ました。

APIの開発が捗る……

パーソナルアクセストークン

いわゆるQiitaやGitHubなんかで利用できる「個人用アクセストークン」です。3rd partyアプリ内での認証が不要で、自分で無効化しないかぎり永続的に利用可能なアクセストークンをユーザー誰でも発行できます。

APIを利用して発行する場合、webで認証済みのセッションが利用可能である場合のみ、それを利用して発行、確認、削除することが出来ます。

personal_access_client=1になっているクライアントが必要ですが、passport:installの際に作成されているはずなので特別な操作は必要ないはずです。

フロントで、/oauth/personal-access-tokens

のようなデータをpostするとアクセストークンが発行されます。
フロントからPOSTする際は上記同様、CSRFトークンをヘッダーに含める必要があるのでお忘れなく。
なお、このトークンはtokensExpireInを無視し、実質無期限なものになります。

issue-parsonal-access-token

発行済のトークン一覧は GET /oauth/personal-access-tokens、削除はdelete /oauth/personal-access-tokens/:idで出来ます。
(いずれもセッションを利用)

取得したトークンを用いてAPIを叩く

ここまで何れかの方法でアクセストークンが取得できれば、これを用いてAPIを叩くことが出来るはずです。

認証情報はAuthorizationヘッダーを用いて渡します。

'Authorization': 'Bearer '+token をヘッダーに追加するだけ。

こんな感じに。
こんな感じに。

どうですか?出来ましたか?

トークンの更新

トークンが期限切れしたら、トークンを更新する必要があります。期限切れには時間経過以外の要因(多重ログインなど)もあるため、時間切れのみを更新のトリガーにしてはいけません。APIを叩いた際に401が返ってきたら、それは更新の合図です。(その更新にも失敗したら、再認証させる必要があるでしょう。)

このようなリクエストを、認証時同様に/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

「Laravel 5.3 で Passportを試す」への4件のフィードバック

  1. ./artisan migrateを実行すると
    DBにはoauth_をプレフィックスとする5つのテーブルが作成されます。とありますが、
    私の環境(centos7,laravel5.4)ではなぜかテーブル作成のエラーが表示されてしまいました。
    結果的に、config/database.phpにある、文字コードをutf8_general_ciに変更すると問題なくmigrateされました。

  2. パスワードを用いてトークンを取得するの項で、POSTするurlが書かれておらず、api/userに投げてしまいました。
    oauth/tokenにpostすると書き加えると良いかと思います。

    1. ご指摘有難うございます。
      すっかり見落としてましたが確かに漏れてますね……

      とりあえず書き足しておきました。(ついでに読み直して引っかかったとことかも修正……)

  3. 上のPostmanを使って(grant-type Authorization code)なんですが
    could not complete oauth2 login というエラーが出てしまいます。

    アドバイスを頂けたらありがたいです。

コメントを残す