Laravel PassportでOAuthサーバーを実装、クライアントアプリでAPIアクセスを確認


OAuthサーバーをLaravel Passportで実装する手順について解説します。

仕事では、Laravelでサービスを運用しているわけではないですが、クラウド プラットフォームのOAuthクライアント機能を調べたりするときにOAuthサーバーがわでデバッグしたいときなどに、Laravel Passportは簡単に実装してサーバーを立てられるのでとても便利に使っています。

以下、Laravelプロジェクトを新規に作るところから、PassportでOAuthサーバーを実装し、クライアントアプリからPassportサーバーに認可コードフローでアクセストークンを取得し、それを使ってAPIからユーザ情報を取得するまで、実装して確認してみます。

Laravel Passportとは

Laravel Passportは、Laravelプロジェクト公式のOAuthサーバー実装です。
OAuthサーバーというとざっくりですが、APIへのアクセス認可とアクセストークンを発行を管理するサーバーのことという理解で良いかと思います。

Laravel Passport provides a full OAuth2 server implementation for your Laravel application in a matter of minutes. Passport is built on top of the League OAuth2 server that is maintained by Andy Millington and Simon Hamp.

ドキュメントに記載されていますが、SPAやモバイルアプリの認証とトークン発行への用途の場合は、Laravel Sanctumの仕様を推奨されています。

Passportを使ってOAuthサーバーを実装

以下、公式ドキュメントのInstallationに沿って、プロジェクトの作成からLaravel Passportのセットアップまでを確認してみます。

空のプロジェクトを用意

まずプロジェクトを新規に作成します。

$ composer create-project --prefer-dist laravel/laravel hello-passport
$ cd hello-passport/

MySQLにデータベースを作成し、.envに設定します。

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=hello-passport
DB_USERNAME=root
DB_PASSWORD=

ここで、マイグレーションを実行して、Laravelフレームワーク自体が提供するテーブルを作成しておきます。

$ php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (55.13ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (49.17ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (48.65ms)
Migrating: 2019_12_14_000001_create_personal_access_tokens_table
Migrated:  2019_12_14_000001_create_personal_access_tokens_table (87.57ms)

Laravel Passportのセットアップ

作成したLaravelプロジェクトにPassportを追加して、設定していきます。

まずは、ComposerでPassportをインストール

$ composer require laravel/passport

マイグレーションを実行して、PassportがOAuthクライアントやアクセストークンを保存するテーブルを作成します。

$ php artisan migrate
Migrating: 2016_06_01_000001_create_oauth_auth_codes_table
Migrated:  2016_06_01_000001_create_oauth_auth_codes_table (102.78ms)
Migrating: 2016_06_01_000002_create_oauth_access_tokens_table
Migrated:  2016_06_01_000002_create_oauth_access_tokens_table (76.57ms)
Migrating: 2016_06_01_000003_create_oauth_refresh_tokens_table
Migrated:  2016_06_01_000003_create_oauth_refresh_tokens_table (76.72ms)
Migrating: 2016_06_01_000004_create_oauth_clients_table
Migrated:  2016_06_01_000004_create_oauth_clients_table (48.49ms)
Migrating: 2016_06_01_000005_create_oauth_personal_access_clients_table
Migrated:  2016_06_01_000005_create_oauth_personal_access_clients_table (23.42ms)

次に、artisanコマンドpassport:installを実行します。
暗号化キーを生成し、クライアントが2つ作られます。

$ php artisan passport:install
Encryption keys generated successfully.
Personal access client created successfully.
Client ID: 1
Client secret: Q205hWpWtuFaZJmQgESXd3vAWEOXhFiLeIFFA0pA
Password grant client created successfully.
Client ID: 2
Client secret: SXIx6agUmeyO16PIztiiaDddbgIzl1vSYa4q1SJB

続いて、いくつかコードを修正していきます。

App\Models\Userを開いて、HasApiTokensトレイトをPassportのものに置き換えます。

// app/Models/User.php

# use Laravel\Sanctum\HasApiTokens;
use Laravel\Passport\HasApiTokens;

App\Providers\AuthServiceProviderを開き、bootメソッドにPassport::routesの呼び出しを追加します。

// app/Providers/AuthServiceProvider.php

...
use Laravel\Passport\Passport;

class AuthServiceProvider extends ServiceProvider
{

    ...

    public function boot()
    {
        $this->registerPolicies();

        if (! $this->app->routesAreCached()) {
            Passport::routes();
        }
    }
}

最後に、config/auth.phpにAPI用のガードを追加して、ドライバーにpassportを指定します。

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        // API用のガードを追加
        'api' => [
            'driver' => 'passport',
            'provider' => 'users',
        ],
    ],

Passportの設定は以上ですが、APIの認証設定を変更しておきます。

routes/api.phpを開き、ミドルウェアをauth:apiに変更しておきます。

// routes/api.php
Route::middleware('auth:api')->get('/user', function (Request $request) {
    return $request->user();
});

認証画面の実装

順番が前後しますが、Passportは、APIへの認可とトークンの発行が担当なので、認証は別途用意する必要があります。
最新の8.xでは、認証用のUIを実装するためのスターターキットのLaravel BreezeまたはLaravel JetStreamを利用することができます。(以前にこのブログでも紹介しましたがLaravel/uiはもう古いようです)。

参考: Starter Kits

ここでは、ミニマムな構成のBreezeを使って認証画面を実装してみます。

公式ドキュメントのLaravel Breeze Installationに沿ってセットアップしてみます。

既存のプロジェクトに追加するので、composerで追加します。

$ composer require laravel/breeze --dev

インストールできたら、artisanコマンドbreeze:installを実行します。

$ php artisan breeze:install
Breeze scaffolding installed successfully.
Please execute the "npm install && npm run dev" command to build your assets.

続いて、npmパッケージのインストールと、アセットのビルドを実行します。

$ npm install && npm run dev
...
✔ Compiled Successfully in 6177ms
┌─────────────────────────────────────────────────────────────────────────────────┬──────────┐
│                                                                            File │ Size     │
├─────────────────────────────────────────────────────────────────────────────────┼──────────┤
│                                                                      /js/app.js │ 710 KiB  │
│                                                                     css/app.css │ 3.83 MiB │
└─────────────────────────────────────────────────────────────────────────────────┴──────────┘
webpack compiled successfully

開発サーバーを起動して動作を確認してみます。

$ php artisan serve

`http://127.0.0.1:8000`を開いて右上の「register」リンクをクリックするとサインナップ画面が開きます。

適当に入力して[REGISTER]をクリックするとユーザが作成されて、ログイン状態になります。

MySQLのusersテーブルを確認するとユーザが追加されていることが確認できます。

クライアントで動作を確認

ここからは、OAuthサーバーを利用するクライアントとなるアプリを作成して、OAuth認証とAPIの呼び出しの流れを確認してみます。
データベースアクセスなども無いので、こちらはLumenでシンプルに作ってみます。

ところで、Laravelで実装する場合は、Laravel Socialiteを使えば良いのですが、ここではOAuth認証とトークン取得の流れを確認するため直にHTTPリクエストを叩くように実装しています。

クライアントアプリのコードは以下のリポジトリで全体を公開していますので参考にしてください。

https://github.com/hrendoh/laravel-passport-client-example/

新規プロジェクトの作成

$ composer create-project --prefer-dist laravel/lumen passport-client
$ cd passport-client

サーバーからのHttpリクエスト実行用にGuzzleをインストールしておきます。

$ composer require guzzlehttp/guzzle

APIクライアントの作成

passport:clientコマンドを使用して、APIクライントを作成できます。既存の編集方法はわかりません。。

$ php artisan passport:client

 Which user ID should the client be assigned to?:
 > 1

 What should we name the client?:
 > Laravel Code Grant Flow        

 Where should we redirect the request after authorization? [http://localhost/auth/callback]:
 > http://localhost:8080/callback

New client created successfully.
Client ID: 3
Client secret: wiNmbsqBoxvnsPvcf4RgeNx5gTtdS9A1mUiA1Ogz

Passportサーバーの認可エンドポイントへリダイレクト

Passportの認可エンドポイントはデフォルトでは「oauth/authorize」です。
以下のように、セッションに「access_token」がなければPassportサーバーの「oauth/authorize」にリダイレクトします。

// PHPのセッションを直に起動
session_start();

$router->get('/', function () use ($router) {
    if (isset($_SESSION['access_token'])) {
        // アクセストトークン取得済みの場合の処理
        ...
    } else {
        return redirect('login');
    }
});

$redirect_url = 'http://'.$_SERVER['SERVER_NAME'].':'.$_SERVER['SERVER_PORT'].'/callback';

$router->get('login', function () use ($router, $redirect_url) {
    $query = http_build_query([
        'client_id' => '3', // 作成したクライアントの「Cliend ID」を指定
        'redirect_uri' => $redirect_url,
        'response_type' => 'code',
        'scope' => '',
    ]);
    // Passportサーバーの認可エンドポイントにリダイレクト
    return redirect('http://localhost:8000/oauth/authorize?' . $query);
});

セッションについては、本来illuminate/sessionを使って実装すべきですがLumenで使うには以下のサイトの手順のようにちょっと面倒なのでPHPのセッションを直に使っています。

参照: rummykhan/lumen-session-example: Enable session in lumen framework (Laravel)

Passportサーバーのトークンエンドポイントからアクセストークンを取得

以下、コールバックの処理です。
GETパラメータ「code」を指定して、アクセストークンを取得し、セッションにセットしています。

$router->get('callback', function () use ($router, $redirect_url) {
    $http = new GuzzleHttp\Client;
    $response = $http->post('http://localhost:8000/oauth/token', [
        'form_params' => [
          'grant_type' => 'authorization_code',
          'client_id' => '3', // 作成したクライアントの「Cliend ID」を指定
          'client_secret' => 'wiNmbsqBoxvnsPvcf4RgeNx5gTtdS9A1mUiA1Ogz', // 作成したクライアントの「Client secret」を指定
          'redirect_uri' => $redirect_url,
          'code' => $_GET['code'],
        ],
    ]);

    $token = json_decode((string)$response->getBody(), true);
    $_SESSION['access_token'] =  $token['access_token'];
    return redirect('/');
});

取得したアクエストークンを使ってAPIを呼び出す

以下、セッションから取得したアクセストークンを指定してPassportサーバーのAPIを呼び出しています。

$router->get('/', function () use ($router) {
    if (isset($_SESSION['access_token'])) {
        $http = new GuzzleHttp\Client;
        $response = $http->request('GET', 'http://localhost:8000/api/user', [
            'headers' => [
                'Accept'     => 'application/json',
                'Authorization' => 'Bearer '.$_SESSION['access_token'],
            ]
        ]);
        $user = json_decode((string)$response->getBody(), true);
        return view('home', ['name' => $user['name']]);
    } else {
        return redirect('login');
    }
});

動作確認

ポート8080でサーバーを起動します。

$ php -S localhost:8080 -t public

「http://localhost/8080」を開くと、Passportサーバーにリダイレクトされます。
ログインすると以下の認可画面が表示されます「Authorize」をクリックすると、コールバックにリダイレクトされます。

コールバックでアクセストークンを取得すると、ルートにリダイレクトされ、Passportサーバーのapi/userからユーザ情報を取得して名前を表示します。

環境変数からキーを読み込む

artisanコマンドvendor:publishを使用して、Passportの設定ファイルをconfigにコピーします。

$ php artisan vendor:publish --tag=passport-config
Copied File [/vendor/laravel/passport/config/passport.php] To [/config/passport.php]
Publishing complete.

config/passport.phpはリポジトリ管理対象となります。

ローカルサーバーでは、.envファイルにPASSPORT_PRIVATE_KEYPASSPORT_PRIVATE_KEYを指定します。

PASSPORT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
<private key here>
-----END RSA PRIVATE KEY-----"

PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
<public key here>
-----END PUBLIC KEY-----"

herokuの例では、config/passport.phpデプロイ後に以下のコマンドで環境変数をセットします。

$ heroku config:set PASSPORT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
> MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCkdGrXv6prTtWX
> m2IeE0P8POBFRQwiRwf9oHoHubrLQR5213XLzhPZ+t8me4jCzU3c4sHqLBBip3Oi...
> 4ZnnFjfokc4ill+qz1GSNcxi6Vur
> -----END PRIVATE KEY-----"
$ heroku config:set PASSPORT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
> MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCkdGrXv6prTtWX
> m2IeE0P8POBFRQwiRwf9oHoHubrLQR5213XLzhPZ+t8me4jCzU3c4sHqLBBip3Oi...
> 4ZnnFjfokc4ill+qz1GSNcxi6Vur
> -----END PRIVATE KEY-----"
$ heroku config:set PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
> MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApHRq17+qa07Vl5tiHhND
> /DzgRUUMIkcH/aB6B7m6y0Eedtd1y84T2frfJnuIws1N3OLB6iwQYqdzovx4BAzo
...
> s/L0tLrApJ+EJJ7aja5+6O0CAwEAAQ==
> -----END PUBLIC KEY-----"

参考: Loading Keys From The Environment

,