Laravel Passportを利用したAPI認証の動作確認をします。Passportはthephpleague/oauth2-serverをLaravelで扱いやすいようにラップしてくれたパッケージです。
事前知識(OAuth 2.0)
OAuth 2.0
の知識が必要です。下記参考になります。
RFC 6749
RFC 6749 The OAuth 2.0 Authorization Framework
(英語) https://tools.ietf.org/html/rfc6749
(日本語) https://openid-foundation-japan.github.io/rfc6749.ja.html
RFC 6750
RFC 6750 The OAuth 2.0 Authorization Framework: Bearer Token Usage
(英語) https://tools.ietf.org/html/rfc6750
(日本語) http://openid-foundation-japan.github.io/rfc6750.ja.html
RFC 6819
RFC 6819 OAuth 2.0 Threat Model and Security Considerations
(英語) https://tools.ietf.org/html/rfc6819
(日本語) http://openid-foundation-japan.github.io/rfc6819.ja.html
IPAの記事
https://www.ipa.go.jp/security/awareness/vendor/programmingv2/contents/709.html
事前準備|laradockで環境構築
macにて、laradockを利用してます。
laravelインストール
workspaceでlaravelをインストールします。
$ docker-compose exec workspace bash
root@6e7be97ae4ee:/var/www# composer create-project laravel/laravel passport 5.4.* --prefer-dist
(省略)
root@6e7be97ae4ee:/var/www# composer create-project laravel/laravel consumer 5.4.* --prefer-dist
(省略)
バーチャルホストの設定
laradockのnginx/sites配下
にて、バーチャルホストごとの設定ファイルを作成します。default.conf
をコピーしてpassport.conf
consumer.conf
を作成します。
listen 80;
listen [::]:80;
server_name passport.dev;
root /var/www/passport/public;
listen 80;
listen [::]:80;
server_name consumer.dev;
root /var/www/consumer/public;
コンテナ再起動
$ docker-compose stop
$ docker-compose up -d nginx mysql
名前解決
ローカルで下記コマンド実行。
sudo vi /etc/hosts
下記内容を追記。
127.0.0.1 passport.dev
127.0.0.1 consumer.dev
laravelの.envを修正
APP_URL=http://passport.dev
APP_URL=http://consumer.dev
このほかに「DBの設定」や「npm install」などしてます。
Passportを実装|サーバサイド
マニュアル通りにPassportを実装してみます。/var/www/passport
で実施してます。
Passportをインストール
composer require laravel/passport
Passportをサービスプロバイダを登録
config/app.php
のproviders配列
に追記します。
'providers' => [
:
Laravel\Passport\PassportServiceProvider::class,
],
Passport用のテーブル生成
Passportをサービスプロバイダに登録したことで、/vendor/laravel/passport/database/migrations配下
のマイグレーションが実行されます。
php artisan migrate
php artisan passport:installを実行
php artisan passport:install
passport:install
を実行することで キーの生成
と レコード挿入
が行われます。
storage/oauth-private.key
storage/oauth-public.key
oauth_personal_access_clientsテーブル
にレコードが追加されました。
oauth_clientsテーブル
にレコードが追加されました。
補足
「Password Grant Client」は以下コマンドでも作成できます。
php artisan passport:client --password
「Personal Access Client」は以下コマンドでも作成できます。
php artisan passport:client --personal
Userモデルへトレイトを追加
UserモデルへLaravel\Passport\HasApiTokensトレイト
を追加します。
UserモデルでPassportのヘルパーメソッドが使えるようになります。
Passport::routesメソッドを追記
App\Providers\AuthServiceProvider
のbootメソッド
に、Passport::routesメソッド
を追記します。
<?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
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
];
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Passport::routes();
}
}
以下のようにルーティングが追加されます。
driverオプションを変更
config/auth.php
にてdriverオプション
を変更します。
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport', // 変更箇所
'provider' => 'users',
],
],
Passportを実装|フロントエンド
Passport用のVueコンポーネント生成
artisanコマンドでPassport用のVueコンポーネント生成します。
php artisan vendor:publish --tag=passport-components
Vueコンポーネントが生成されました。
生成したコンポーネントを登録
resources/assets/js/app.js
に生成したVueコンポーネントを登録します。
Vue.component(
'passport-clients',
require('./components/passport/Clients.vue')
);
Vue.component(
'passport-authorized-clients',
require('./components/passport/AuthorizedClients.vue')
);
Vue.component(
'passport-personal-access-tokens',
require('./components/passport/PersonalAccessTokens.vue')
);
アセットを再コンパイル
再コンパイルします。
npm run dev
welcome.bladeにコンポーネント設定
動作確認したいので、welcome.blade
を以下のように修正しました。
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<link rel="stylesheet" href="css/app.css"/>
<script type="text/javascript">
window.Laravel = window.Laravel || {};
window.Laravel.csrfToken = "{{csrf_token()}}";
</script>
</head>
<body>
<div id="app">
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<passport-clients></passport-clients>
<passport-authorized-clients></passport-authorized-clients>
<passport-personal-access-tokens></passport-personal-access-tokens>
</div>
</div>
</div>
</div>
<script src="js/app.js"></script>
</body>
</html>
以上で、Passportの実装が完了です。
動作確認|Access Tokenの取得
( Authorization Code Grant )
4つの承認タイプ
OAuth2.0には4つの承認タイプが定義されています。
承認タイプ | 概要 |
---|---|
Authorization Code Grant | PHPなどバックエンドからOAuthを利用する場面に使われる。 |
Implicit Grant | JavaScriptなどフロントエンドからOAuthを利用する場面に使われる。 |
Resource Owner Password Credentials Grant | Resource OwnerのパスワードをClientサービスが知ってしまうデメリットあり。 |
Client Credentials Grant | 信頼できるマシン間でOAuthする際に利用する場面に使われる。 |
ここでは、Authorization Code Grant
の動作確認を行います。
consumerアプリケーションの登録
ユーザー登録 & ログイン
Clientサービスを登録するには、ログインしている必要があります。認証機能をまだ実装してなかったので、var/www/passport
にて下記コマンドを実行します。
php artisan make:auth
http://passport.dev/register
にアクセスして、ユーザーを登録します。
Clientサービスを登録
http://passport.dev/
にアクセスして、「Create New Client」をクリックします。
Clientサービス名
とCallback
を入力して、Createをクリックします。
これにより、oauth_clientsテーブル
にレコードが追加されました。
Client ID
と Secret
をconsumerアプリで利用します。
consumerアプリケーションの実装
/var/www/consumer
で実施します。
routes/web.php
に下記処理を記述します。
<?php
use Illuminate\Http\Request;
Route::get('/', function () {
$query = http_build_query([
'client_id' => '4',
'redirect_uri' => 'http://consumer.dev/callback',
'response_type' => 'code',
'scope' => '',
]);
return redirect('http://passport.dev/oauth/authorize?'.$query);
});
Route::get('/callback', function (Request $request) {
$http = new GuzzleHttp\Client;
$response = $http->post('http://passport.dev/oauth/token', [
'form_params' => [
'grant_type' => 'authorization_code',
'client_id' => '4',
'client_secret' => 'N014nweB8YFHmCanDduGRXCLUm1O69Dpp9U52qjM',
'redirect_uri' => 'http://consumer.dev/callback',
'code' => $request->code,
],
]);
return json_decode((string) $response->getBody(), true);
});
guzzleを追加でインストールしてます。
composer require guzzlehttp/guzzle
注意|Guzzleでpassport.devにアクセスできない!
ここで紹介しているlaradockを利用した環境ですと、このままでは、Guzzleでpostメソッドを使った際に、下記エラーがでます。
cURL error 7: Failed to connect to passport.dev port 80: Connection refused
php-fpmのコンテナ上で、nginxのコンテナに対する名前解決をまだしていないためです。
他に上手い解決方法がありそうですが、ここでは、docker-compose.yml
を以下のよう修正しました。
php-fpm:
extra_hosts:
- "dockerhost:${DOCKER_HOST_IP}"
- "passport.dev:172.18.0.5"
extra_hosts
に追記することで、php-fpmコンテナの「/etc/hosts」が更新されます。passport.devには、NginxのコンテナのIPを指定してます。コンテナのIPは、固定じゃないので、その場しのぎの対応になります。
下記コマンドで設定変更を反映させます。
docker-compose build --no-cache php-fpm workspace
動作確認
AuthorizationCode取得
→ AccessToken取得
の流れを動作確認します。
AuthorizationCodeを取得
http://consumer.dev
にアクセスすると、下記URLにリダイレクトされます。
http://passport.dev/oauth/authorize?client_id=4&redirect_uri=http%3A%2F%2Fconsumer.dev%2Fcallback&response_type=code&scope=
ここで、Cancelをクリックすると以下のようにredirectされます。
http://consumer.dev/callback?error=access_denied&state=
認証コードが含まれてませんね。
Authorizeをクリックすると以下のようにredirectされます。
http://consumer.dev/callback?code=vaYN2ILCkROqEUObNFp7GttdTlbfvQMFCz3qiyqWxqTohzzumA14fnJmE6vmHMVXpbS%2BMNioU%2BgD0NCLumHh87GxIRBTkSOKp%2FyNwAIDQmNdSQg6NCjK8WaIV1GOKZUQZIgeTU8Y%2FC4s1i4KFB7A%2BCp75zqEuNKfij1e0CsTQdfQdlp7VsjPM9JyFwewlC2El17xAa6KWI%2F8GNL6LADB9X5mxe9cFTjFqH5MhK%2FL7uSHkYEHsmQcdRRC27DO%2BtRnJ8Ddw1W1%2BXbD1g3B3%2BJA5mZihudSVlKKyzxJ%2B%2Fm5Iur3uWPLhRDA55v9rD6LNub5RXK7MApM0FbgW%2B06Qv6A%2FRok1hzh5rPif5dB943oYkyNuf1nej2AznJHi8oTu%2Fb8n7GoMGnzrgKwH0KUY1qXUBl8e9Yw7IwnYGTgC%2BxIlDN740o3xkajqtHtZRcdt842nUVDPhe2OfI6LJICSS7PpsszEw4cKlgipTuw1kz9BmoiCGI1rFkboZq%2F5rnGEnR6BNLEezNXsztS2iSmevyLqRRoLlIHxinp1gkGHTp24KHXH2S3s2Xr4nOXXbHXo650CrdIurOn%2FNWZiEgOULkfjTrZEy%2Bq6%2FOQDzR0kTIP2GP9jF2RzFde37sTKzmY4Fs7P%2BnFHc%2Be0CwqBLwcTSXyq5T56oEXBhu4%2BfzTkkq7hJI%3D
認証コードが含まれています。
この時点で、oauth_auth_codesテーブル
に認証コードが追加されています。
AccessTokenを取得
http://consumer.dev/callback
では、リクエストデータから認証コードを取得して、Guzzleで http://passport.dev/oauth/token
に対しPOSTメソッドを実行しています。これにより、oauth_access_tokensテーブル
にAccess_Tokenが追加されます。
同様に、oauth_refresh_tokensテーブル
にRefreshTokenが追加されます。
http://consumer.dev/callback
では、Guzzleで取得した情報をレスポンスしており、以下内容が表示されたことを確認できました。
{"token_type":"Bearer","expires_in":31536000,"access_token":"(省略)","refresh_token":"(省略)"}
下記パラメータが含まれてます。
- token_type
- expires_in
- access_token
- refresh_token
動作確認|Access Tokenで認証ガード
passport.dev側の設定
デフォルトで http://passport.dev/api/user
のルーティングが routes/api.php
で定義されています。
Route::middleware('auth:api')->get('/user', function (Request $request) {
return $request->user();
});
ミドルウェアに auth:api
が指定されているため、Access Tokenによる認証ガードが行われます。
consumer.dev側の設定
routes/web.php
に下記処理を追記します。
Route::get('/api_test', function (Request $request) {
$client = new GuzzleHttp\Client;
$accessToken = "取得したAccess Token";
$response = $client->request('GET', 'http://passport.dev/api/user', [
'headers' => [
'Accept' => 'application/json',
'Authorization' => 'Bearer ' . $accessToken,
],
]);
return json_decode((string)$response->getBody(), true);
});
動作確認
http://consumer.dev/api_test
にアクセスします。
Access Tokenが有効なとき
次のようにユーザー情報を取得できました。
{"id":1,"name":"test","email":"test@test.com","created_at":"2017-03-26 06:29:46","updated_at":"2017-03-26 06:29:46"}
Access Tokenが無効なとき
下記エラーが発生しました。無効なときは、401
を返します。
Client error: `GET http://passport.dev/api/user` resulted in a `401 Unauthorized` response:
{"error":"Unauthenticated."}