API認証(Passport)機能の動作確認

Tag:

Laravel Passportを利用したAPI認証の動作確認をします。

Passportはthephpleague/oauth2-serverをLaravelで扱いやすいようにラップしてくれたパッケージです。

事前知識(OAuth 2.0)

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 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 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

環境

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
変更箇所

    listen 80;
    listen [::]:80;

    server_name passport.dev;
    root /var/www/passport/public;
consumer.conf
変更箇所

    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を修正

passportの方

APP_URL=http://passport.dev

consumerの方

APP_URL=http://consumer.dev

このほかに「DBの設定」や「npm install」などしてます。

Passportを実装

マニュアル通りにPassportを実装してみます。「/var/www/passport」で実施してます。

サーバエンド

1 Passportをインストール
composer require laravel/passport
2 Passportをサービスプロバイダを登録
config/app.phpのproviders配列に追記

'providers' => [
                  :
        Laravel\Passport\PassportServiceProvider::class,
    ],
3 Passport用のテーブル生成
php artisan migrate

Passportをサービスプロバイダに登録したことで、「/vendor/laravel/passport/database/migrations」配下のマイグレーションが実行されます。

4 php artisan passport:installを実行
php artisan 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
5 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;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];
}

5,11行目を修正してます。
UserモデルでPassportのヘルパーメソッドが使えるようになります。

6 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();
    }
}

5,29行目を追記しています。

以下のようにルーティングが追加されます。

7 config/auth.phpにてdriverオプションを変更
    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

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

8行目ですね。

フロントエンド

1 ArtisanコマンドでPassport用のVueコンポーネント生成
php artisan vendor:publish --tag=passport-components

上記コマンドを実行することで、下記のようにVueコンポーネントが生成されました。

2 生成したコンポーネントを登録
resources/assets/js/app.jsファイルに生成したVueコンポーネントを登録します。


/**
 * First we will load all of this project's JavaScript dependencies which
 * includes Vue and other libraries. It is a great starting point when
 * building robust, powerful web applications using Vue and Laravel.
 */

require('./bootstrap');

/**
 * Next, we will create a fresh Vue application instance and attach it to
 * the page. Then, you may begin adding components to this application
 * or customize the JavaScript scaffolding to fit your unique needs.
 */

Vue.component('example', require('./components/Example.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')
);

const app = new Vue({
    el: '#app'
});

17~30行目を追記しています。

3 アセットを再コンパイル
下記コマンドで再コンパイルします。

npm run dev

4 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の取得まで

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の動作確認を行います。

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は、固定じゃないですが172.18.0.5になることが多かったので、、、
よくない対応ですが、Dockerは別の機会に勉強します、、、

下記コマンドで設定変更を反映させます。

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."}

参考URL

https://readouble.com/laravel/5.4/ja/passport.html

https://laracasts.com/series/whats-new-in-laravel-5-3/episodes/13

https://www.youtube.com/watch?list=PLkZU2rKh1mT9TgMvpFY1QVrX2z4I3D3Jq&v=cRCm8-cXD-w

https://speakerdeck.com/hinaloe/passportdehazimeruoauth2-number-laravel-osaka

スポンサーリンク