#Laravel5
でOAuth2
を実装する
Laravel5
でOAuth2
の実装する場合、laravel-passport
またはoauth2-server-laravel
が利用できる。
これらの導入を検討をした際に調査した内容をメモとして残す(かなり古い情報で申し訳ないです)。
##composer.json
composer.json
に下記を追記。
laravel/passport
または、lucadegasperi/oauth2-server-laravel
いずれかを記載(両方記載は不可)。
"require": {
...
"lucadegasperi/oauth2-server-laravel": "5.2.*",
"laravel/passport": "4.0.*",
"zircote/swagger-php": "2.0.*"
},
composer update
を実行してインストール。
##アカウントの用意
APIを利用するユーザ情報を登録するため、Laravel
標準のAuth
を使う。
$ php artisan make:auth
ユーザを簡易的に登録する場合、Seederを利用する。
$ php artisan make:seeder UsersTableSeeder
上記を実行すると、database/seeds/UsersTableSeeder.php
が作成されるので、下記のように編集する。
<?php
use Illuminate\Database\Seeder;
class UsersTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
DB::table('users')->insert([
'name' => 'test_user',
'email' => 'test_user@gmail.com',
'password' => bcrypt('secret'),
]);
}
}
##OAuth2の実装
larave/passport
を利用する場合
larave/passport
の詳細については、詳細はこちら。
####初期設定
#####config/app.php
の編集
config/app.php
のproviders
に下記を追加。
'providers' => [
Laravel\Passport\PassportServiceProvider::class,
],
#####マイグレーション
php artisan migrate
を実行する。
#####「パーソナルアクセス」クライアントと「パスワードグラント」クライアントの作成
passport
のインストールコマンドを実行する。
$ php artisan passport:install
#####App\User
モデルの編集
App\User
モデルへLaravel\Passport\HasApiTokens
を追加する。
<?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;
}
#####app/Providers/AuthServiceProvider.php
の追記
app/Providers/AuthServiceProvider.php
に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();
}
}
#####config/auth.php
の編集
config/auth.php
設定ファイル中で、ガードのapi認証のdriver
オプションをpassport
へ変更する。
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],
####暗号化キーの作成
稼働サーバに(はじめて)デプロイしたときは、php artisan passport:keys
を実行し、暗号化キーを生成する。
(忘れることが多いので、何度かはまりました。)
####トークン持続時間
アクセストークン、リフレッシュトークンの持続時間はapp/Providers/AuthServiceProvider.php
を編集して設定する。
use Carbon\Carbon;
/**
* 全認証/認可サービスの登録
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Passport::routes();
Passport::tokensExpireIn(Carbon::now()->addDays(15));
Passport::refreshTokensExpireIn(Carbon::now()->addDays(30));
}
####クライアントID、クライアントシークレットの発行
#####コマンドでの実行
php artisan passport:client --password
を実行すると、ダイアログ式でクライアント情報を作成し、クライアントIDとクライアントシークレットの発行ができる。
なお、ダイアログ式で作成する場合、Users
テーブルのid
を指定するシナリオがある。
#####UIからの実行
######vue.jsのモジュール作成
php artisan vendor:publish --tag=passport-components
を実行することで、クライアン情報をUIで作成できる。
######モジュールのビルド
resources/assets/js/app.js
に下記を追記する。
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')
);
Laravelプロジェクトのルートディレクトリで下記を実行し、app.js
をビルドする。
npm
かyarn
は各自でインストールする。
$ npm init
$ npm run dev
######テンプレートの編集
該当テンプレートで、下記を呼び出す。
{{-- クライント管理 --}}
<passport-clients></passport-clients>
{{-- トークン管理 --}}
<passport-personal-access-tokens></passport-personal-access-tokens>
#####JSON API
からの実行
######GET /oauth/clients
認証ユーザの一覧のクライアントID・クライアントシークレットのデータを取得。
axios.get('/oauth/clients')
.then(response => {
console.log(response.data);
});
######POST /oauth/clients
認証ユーザがクライアントID・クライアントシークレットを発行する。
const data = {
name: 'Client Name',
redirect: 'http://example.com/callback'
};
axios.post('/oauth/clients', data)
.then(response => {
console.log(response.data);
})
.catch (response => {
// レスポンス上のエラーのリスト
});
######PUT /oauth/clients/{client-id}
認証ユーザがクライアント情報を更新する。
const data = {
name: 'New Client Name',
redirect: 'http://example.com/callback'
};
axios.put('/oauth/clients/' + clientId, data)
.then(response => {
console.log(response.data);
})
.catch (response => {
// レスポンス上のエラーのリスト
});
######DELETE /oauth/clients/{client-id}
作成したクライアント情報を削除。
axios.delete('/oauth/clients/' + clientId)
.then(response => {
//
});
####アクセストークンの取得
#####初回
以下のような/oauth/token
へのPOSTリクエストで、アクセストークンを取得できる。
$http = new GuzzleHttp\Client;
$response = $http->post('http://your-app.com/oauth/token', [
'form_params' => [
'grant_type' => 'password',
'client_id' => 'client-id',
'client_secret' => 'client-secret',
'username' => 'taylor@laravel.com',
'password' => 'my-password',
'scope' => '',
],
]);
return json_decode((string) $response->getBody(), true);
レスポンスデータは下記。
{
"token_type": "Bearer",
"expires_in": 31536000,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6IjNjMTQ5ZWU4ZTFkZTc4YmRmZDQwODVkNTk3OTExZmU0Yzk2OTA4MDMxYmQyMDk3NGEzMjljZDI3NzgyMDY5NDA0NzE3NDVmMjUzNzVjODIyIn0.eyJhdWQiOiI1IiwianRpIjoiM2MxNDllZThlMWRlNzhiZGZkNDA4NWQ1OTc5MTFmZTRjOTY5MDgwMzFiZDIwOTc0YTMyOWNkMjc3ODIwNjk0MDQ3MTc0NWYyNTM3NWM4MjIiLCJpYXQiOjE1MDg3NTY4OTEsIm5iZiI6MTUwODc1Njg5MSwiZXhwIjoxNTQwMjkyODkxLCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.EnnFh6saCPzPQDD9x5RpWuvE6Glp5QDidph7RBqS3ok2n4XIhGxDfshcVRrdn68CWm1etCJPrB_rypmQi_sR2CV391Y69k-DQj38qoX3SFHBGzNUlRaSWBmUsL9RMNXDuFEryhcnUuPRn2NPN1wpHIFKxfseidZRSp-hwd2C8rx5ZKw_E7DuN6Ckq1OLcR58X8_5rPGTzaAy-NyinR0LunlENVYreqCdurx6_QURtFddzcd8oEXlPH3VsN2xGl93B_9uR8CU-DGvcNJVb3TlSlmQGf3mnPW0DzkymQSEA4SKhWPQxAjxgDoqX8B89wMktsUDCXahB2ZLXy6hKLKdv73Nq3PJGRTvD3aDisDAaXOwsA_6pNkDVzMJXA6PVj53meXcfSZ-xl6G1vqKZwkgUYoFWF9zgOoK34FmeT_ZDl7FSdRt9SEAP4BYlUKes4lj0w6MG4R4QwhtWh63q8igG295I2rPpId1mNjYC9IiscH9jwSBfqroEdhYWyVXOKEB6DaS5MzYh6ch5m32gzuFYMpcYq8bHfqW-Ki_s9E0lzC3iLEIxtbPdr3xN1c_Pju7DhECYyPBEMFgHSxccwOZtbVB0sbdvyMcFsAAv3SaM7dAQSXAdleAhqfL8-XJ3GK2Kt5SE0QBKRaUDC8v-MTwsS4KIJ0Cec32RULgri9tel4",
"refresh_token": "def502000fa1446806a9f12e648442091b5feaf68cd7df774adc5b6d455cce35c2018f7262a8368422bd669fa60c32b23b2b405fdd65039970aae46295b66ca43c90dea85a660ad1ad75da46b5a14d087fabaf3436483c7fcfe3283e0383215ee756c49d39975658e3fa69b3596a8f35844b1965db6a4d7a66abc5997cec6773a9def65e27ae6b4a62b4f28f6c709256598d0903ed71d6252ced8c34d0ed55b94bf1261464c765bce32c855440ce360fc993efd36623c2501e8b78e2e89c10f3e1650e0d078ea033dcd6e75e066193839705b91c4c3c8df7644ec1a8446a80973bc012deaa00044b8db0127a7444a3d5427a1450ab298f209f009351209f678554047836f081ad73b3fdf76b82fc5d6defee9ea496fa1713fc001e72ec7d9522113da0ade1c8668ddd4985c2c271fa99f56d86c02f59a1d2c2215efd16f0b23305cb0cb62c037bfb190244926addb3749449111fec95003fc008e290d5e7aa5a4f"
}
#####再取得
アクセストークンを再取得(リフレッシュ)するには以下のように/oauth/token
へPOSTする。
$http = new GuzzleHttp\Client;
$response = $http->post('http://your-app.com/oauth/token', [
'form_params' => [
'grant_type' => 'refresh_token',
'refresh_token' => 'the-refresh-token',
'client_id' => 'client-id',
'client_secret' => 'client-secret',
'scope' => '',
],
]);
return json_decode((string) $response->getBody(), true);
レスポンスデータは以下。
{
"token_type": "Bearer",
"expires_in": 31536000,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6IjNjMTQ5ZWU4ZTFkZTc4YmRmZDQwODVkNTk3OTExZmU0Yzk2OTA4MDMxYmQyMDk3NGEzMjljZDI3NzgyMDY5NDA0NzE3NDVmMjUzNzVjODIyIn0.eyJhdWQiOiI1IiwianRpIjoiM2MxNDllZThlMWRlNzhiZGZkNDA4NWQ1OTc5MTFmZTRjOTY5MDgwMzFiZDIwOTc0YTMyOWNkMjc3ODIwNjk0MDQ3MTc0NWYyNTM3NWM4MjIiLCJpYXQiOjE1MDg3NTY4OTEsIm5iZiI6MTUwODc1Njg5MSwiZXhwIjoxNTQwMjkyODkxLCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.EnnFh6saCPzPQDD9x5RpWuvE6Glp5QDidph7RBqS3ok2n4XIhGxDfshcVRrdn68CWm1etCJPrB_rypmQi_sR2CV391Y69k-DQj38qoX3SFHBGzNUlRaSWBmUsL9RMNXDuFEryhcnUuPRn2NPN1wpHIFKxfseidZRSp-hwd2C8rx5ZKw_E7DuN6Ckq1OLcR58X8_5rPGTzaAy-NyinR0LunlENVYreqCdurx6_QURtFddzcd8oEXlPH3VsN2xGl93B_9uR8CU-DGvcNJVb3TlSlmQGf3mnPW0DzkymQSEA4SKhWPQxAjxgDoqX8B89wMktsUDCXahB2ZLXy6hKLKdv73Nq3PJGRTvD3aDisDAaXOwsA_6pNkDVzMJXA6PVj53meXcfSZ-xl6G1vqKZwkgUYoFWF9zgOoK34FmeT_ZDl7FSdRt9SEAP4BYlUKes4lj0w6MG4R4QwhtWh63q8igG295I2rPpId1mNjYC9IiscH9jwSBfqroEdhYWyVXOKEB6DaS5MzYh6ch5m32gzuFYMpcYq8bHfqW-Ki_s9E0lzC3iLEIxtbPdr3xN1c_Pju7DhECYyPBEMFgHSxccwOZtbVB0sbdvyMcFsAAv3SaM7dAQSXAdleAhqfL8-XJ3GK2Kt5SE0QBKRaUDC8v-MTwsS4KIJ0Cec32RULgri9tel4",
"refresh_token": "def502000fa1446806a9f12e648442091b5feaf68cd7df774adc5b6d455cce35c2018f7262a8368422bd669fa60c32b23b2b405fdd65039970aae46295b66ca43c90dea85a660ad1ad75da46b5a14d087fabaf3436483c7fcfe3283e0383215ee756c49d39975658e3fa69b3596a8f35844b1965db6a4d7a66abc5997cec6773a9def65e27ae6b4a62b4f28f6c709256598d0903ed71d6252ced8c34d0ed55b94bf1261464c765bce32c855440ce360fc993efd36623c2501e8b78e2e89c10f3e1650e0d078ea033dcd6e75e066193839705b91c4c3c8df7644ec1a8446a80973bc012deaa00044b8db0127a7444a3d5427a1450ab298f209f009351209f678554047836f081ad73b3fdf76b82fc5d6defee9ea496fa1713fc001e72ec7d9522113da0ade1c8668ddd4985c2c271fa99f56d86c02f59a1d2c2215efd16f0b23305cb0cb62c037bfb190244926addb3749449111fec95003fc008e290d5e7aa5a4f"
}
###oauth2-server-laravel
を利用する場合
####初期設定
config/app.php
に以下を追記。
'providers' => [
...
LucaDegasperi\OAuth2Server\Storage\FluentStorageServiceProvider::class,
LucaDegasperi\OAuth2Server\OAuth2ServerServiceProvider::class,
],
'aliases' => [
...
'Authorizer' => LucaDegasperi\OAuth2Server\Facades\Authorizer::class,
],
app/Http/Kernel.php
に以下を追記。
また、Laravel標準のCSRFを無効にするため、$middlewareGroups
にある\App\Http\Middleware\VerifyCsrfToken::class
を削除。
protected $middleware = [
...
\LucaDegasperi\OAuth2Server\Middleware\OAuthExceptionHandlerMiddleware::class,
];
protected $routeMiddleware = [
...
'csrf' => \App\Http\Middleware\VerifyCsrfToken::class,
'oauth' => \LucaDegasperi\OAuth2Server\Middleware\OAuthMiddleware::class,
'oauth-user' => \LucaDegasperi\OAuth2Server\Middleware\OAuthUserOwnerMiddleware::class,
'oauth-client' => \LucaDegasperi\OAuth2Server\Middleware\OAuthClientOwnerMiddleware::class,
'check-authorization-params' => \LucaDegasperi\OAuth2Server\Middleware\CheckAuthCodeRequestMiddleware::class,
];
####テーブル作成と設定ファイルの作成
php artisan vendor:publish
を実行し、マイグレーションファイルとconfig/oauth2.php
を作成する。
php artisan migrate
を実行し、14のテーブルを作成。
####Password Grant
#####設定
config/oauth2.php
に下記を追記。
'grant_types' => [
'password' => [
'class' => '\League\OAuth2\Server\Grant\PasswordGrant',
'callback' => '\App\PasswordGrantVerifier@verify',
'access_token_ttl' => 3600 //sec
]
],
#####認証処理の追加
app/Http/Controllers/PasswordGrantVerifier.php
を新規作成。
namespace App;
use Illuminate\Support\Facades\Auth;
class PasswordGrantVerifier
{
public function verify($username, $password)
{
$credentials = [
'email' => $username,
'password' => $password,
];
if (Auth::once($credentials)) {
return Auth::user()->id;
}
return false;
}
}
#####ユーザ情報の追加
Seederで実現する場合、php artisan make:seeder OauthClientsTableSeeder
を実行し、作成されたSeederを編集。
こちらも、ユーザと同様に管理画面を作成する。
<?php
use Illuminate\Database\Seeder;
// use Hash;
class OauthClientsTableSeeder extends Seeder
{
/**
* Run the database seeds.
* php artisan db:seed --class=OauthClientsTableSeeder
* @return void
*/
public function run()
{
DB::table('oauth_clients')->insert([
'id' => 'abcdefghijklmiyOD9b6VQ81YTg5bNTRnqfPR2Lf',
'secret' => 'ABCDEFGHIJKLM2K7bOfgrYmMqfkEvPb8sWti0yOSz',
'name' => 'test_client',
]);
}
}
php artisan db:seed --class=OauthClientsTableSeeder
を実行する。
####Refresh Token Grant
アクセストークンの期限が切れた時に利用する。
#####設定
config/oauth2.php
に下記を追加。
'grant_types' => [
'refresh_token' => [
'class' => '\League\OAuth2\Server\Grant\RefreshTokenGrant',
'access_token_ttl' => 3600, //sec
'refresh_token_ttl' => 36000 //sec
]
],
#####ルーティングの設定
routes/web.php
に下記を追記する。
Route::post('oauth/access_token', function() {
return Response::json(Authorizer::issueAccessToken());
});
#####Access Tokenの取得
下記の内容でPOST。
grant_type:password
client_id: abcdefghijklmiyOD9b6VQ81YTg5bNTRnqfPR2Lf
client_secret: ABCDEFGHIJKLM2K7bOfgrYmMqfkEvPb8sWti0yOSz
username:xxxx@xxxx.com
password:xxxx
レスポンスデータは以下。
{
"access_token": "3YMn3WdkJCMiiHaYjeYzpyYpBWALF2iaaipUiNgJ",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "wZ8nkBXJU2i9Z0Vqkn7odXbq2WlUP2vURzXbip03"
}
####Access Tokenの再取得
下記の内容でPOST。
grant_type : refresh_token
client_id : abcdefghijklmiyOD9b6VQ81YTg5bNTRnqfPR2Lf
client_secret : ABCDEFGHIJKLM2K7bOfgrYmMqfkEvPb8sWti0yOSz
refresh_token : qgk1FdsbFu325p54IBdIahXFD635Nb0EA5QH4zqm
レスポンスデータは以下。
{
"access_token": "I6r6CvC6Ld5aRRvS4dFWZc0ov0Ujmq28YIQqnTpI",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "rDfEPr6iGz7AsIsVE27rmUhyDhJU8xCJ8qEuV4xh"
}
##Swagger
###仕様
Swagger
についてはこちらを参照。
###ルーティング
下記例のように、OAuth
でエンドポイントを保護する。
ProductController
についてはSwagger
の項目に記載。
####laravel/passport
の場合
Route::group(['middleware' => [''auth:api'']], function () {
Route::post('/api/products', 'Api\ProductController@index');
});
####oauth2-server-laravel
の場合
Route::group(['middleware' => ['oauth']], function () {
Route::post('/api/products', 'Api\ProductController@index');
});
APIはリクエストヘッダ情報にAuthorization: Bearer d9lZ1vp40v47azMFmhWuGZTG9C4DVtJzICK1LErZ
のようにアクセストークンを付与する。
###戻り値の規約
下記のように返却する。
また、Access Tokenの(再)取得についても従うようにする。
{
"result":{},
"message":"",
"type":"success",
"status":0
}
result
:ハッシュまたは配列
message
:レスポンに対するメッセージ
type
:レスポンス種別(success
、failed
等)
status
:ステータスコード(別途定義が必要)
###API基本情報
app
ディレクトリ直下に基本情報app/swagger.php
を作成する。
<?php
/**
* @SWG\Swagger(
* schemes={"http"},
* host="localhost",
* basePath"="/api",
* @SWG\Info(
* title="My first swagger documented API",
* version="1.0.0"
* )
* )
*/
###モデルのスキーマ定義
下記がモデルの例。
<?php
// app/Models/Product.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @SWG\Definition()
*/
class Product extends Model
{
/**
* The product id
* @var integer
*
* @SWG\Property()
*/
public $id;
/**
* The product name
* @var string
*
* @SWG\Property()
*/
public $name;
}
###コントローラのAPI定義
<?php
// app/Http/Api/ProductController
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Product;
class ProductController extends Controller
{
/**
* @SWG\Post(
* path="/api/products",
* summary="list products",
* @SWG\Response(
* response=200,
* description="A list with products",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/Product")
* )
* ),
* @SWG\Response(
* response=500,
* description="an unexpected error"
* )
* )
*/
public function index()
{
return response()->json([
'result' => [
'statistics' => [
'products' => Product::all()
],
],
'message' => '',
'type' => 'success',
'status' => 0
]);
}
}
###swagger jsonの作成
public/api/
配下に作成。
$ ./vendor/bin/swagger ./app --output ./public/api/
###swagger-ui
のダウンロード
swagger-uiをダウンロードし、dist
配下をpublic/swagger
配下にコピー
http://localhost/swagger/index.html
でhttp://localhost/api/swagger.json
を"Explore"に指定すると、APIドキュメントの参照が可能。
##まとめ
Laravel5.3から標準のlaravel-passport
を利用でき、実装も比較的容易。
しかし、下記の仕様に従う必要がある。
- クライアント情報(クライアントID・シークレット)を利用者側が発行する必要がある
-
Password Grant
を採用するが、冗長なコールバックURLを指定する必要がある(不要にするにはカスタマイズが必要)
上記1.についてはoauth2-server-laravel
を利用することで、クライアント情報を運営者側で管理しやすくなるが、Laravel5.4では推奨されていないモジュール扱いになる。
しかもクライアント情報の管理画面を開発する必要がある(laravel-passport
ではある程度の雛形的なパーツはそろっている)。
##その他
###API検証時のツール
APIの検証用として、POSTMANというChrome拡張がある。
###Laravelのマイグレーション
マイグレーションファイル作成。
$ php artisan make:migration create_users_table --create=users
マイグレーションファイル編集後に以下を実行。
$ php artisan migrate
直前のマイグレーションのロールバックは以下。
$ php artisan migrate:rollback