search
LoginSignup
11

More than 3 years have passed since last update.

posted at

Laravel5でOAuth2とSwagger

Laravel5OAuth2を実装する

Laravel5OAuth2の実装する場合、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が作成されるので、下記のように編集する。

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.phpprovidersに下記を追加。

config/app.php
   '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.phpPassport::routes();を追記する。

app/Providers/AuthServiceProvider.php
<?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へ変更する。

config/auth.php
    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'passport',
            'provider' => 'users',
        ],
    ],

暗号化キーの作成

稼働サーバに(はじめて)デプロイしたときは、php artisan passport:keysを実行し、暗号化キーを生成する。
(忘れることが多いので、何度かはまりました。)

トークン持続時間

アクセストークン、リフレッシュトークンの持続時間はapp/Providers/AuthServiceProvider.phpを編集して設定する。

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に下記を追記する。

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をビルドする。
npmyarnは各自でインストールする。

$ 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に以下を追記。

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を削除。

app/Http/Kernel.php
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に下記を追記。

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 を新規作成。

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に下記を追加。

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に下記を追記する。

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:レスポンス種別(successfailed等)
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.htmlhttp://localhost/api/swagger.jsonを"Explore"に指定すると、APIドキュメントの参照が可能。

まとめ

Laravel5.3から標準のlaravel-passportを利用でき、実装も比較的容易。
しかし、下記の仕様に従う必要がある。

  1. クライアント情報(クライアントID・シークレット)を利用者側が発行する必要がある
  2. 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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
11