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

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.phpproviders配列に追記します。

'providers' => [
                  :
        Laravel\Passport\PassportServiceProvider::class,
    ],

Passport用のテーブル生成

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

php artisan migrate
laravel_passport1.png

php artisan passport:installを実行

php artisan passport:install
laravel_passport2.png

passport:installを実行することで キーの生成レコード挿入 が行われます。

storage/oauth-private.key
storage/oauth-public.key

oauth_personal_access_clientsテーブル にレコードが追加されました。

oauth_personal_access_clients.png

oauth_clientsテーブル にレコードが追加されました。

oauth_clients-768x61.png

補足

「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\AuthServiceProviderbootメソッドに、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();
    }
}

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

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コンポーネントが生成されました。
laravel_passport4.png

生成したコンポーネントを登録

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
laravel_passport5.png

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の実装が完了です。

laravel_passport6.png

動作確認|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 にアクセスして、ユーザーを登録します。

laravel_passport_ope_chk_1.png

Clientサービスを登録

http://passport.dev/ にアクセスして、「Create New Client」をクリックします。

Clientサービス名Callbackを入力して、Createをクリックします。

laravel_passport_ope_chk_2.png

これにより、oauth_clientsテーブルにレコードが追加されました。

ope_chk_oauth_clients-768x65.png

Client IDSecret をconsumerアプリで利用します。

laravel_passport_ope_chk_3.png

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=
laravel_passport_ope_chk_4.png

ここで、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テーブルに認証コードが追加されています。

oauth_auth_codes.png

AccessTokenを取得

http://consumer.dev/callback では、リクエストデータから認証コードを取得して、Guzzleで http://passport.dev/oauth/token に対しPOSTメソッドを実行しています。これにより、oauth_access_tokensテーブル にAccess_Tokenが追加されます。

oauth_access_tokens-768x43.png

同様に、oauth_refresh_tokensテーブルにRefreshTokenが追加されます。

oauth_refresh_tokens.png

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

わくわくBank.
技術系の記事を中心に、役に立つと思ったこと、整理したい情報などを掲載しています。