Laravel Socialite でMicrosoft Graph (Office 365アカウント)認証する

前回の記事 「Laravel 5.8からMicrosoft Graph APIを利用する (公式ドキュメント編)」では、Micsoroft公式のチュートリアル ドキュメントに沿って、LaravelアプリケーションからMicrosoft Graph APIをOAuth認証プロバイダとして利用する手順について確認しました。

このチュートリアルは、OAuthの認証ロジックをleague/oauth2-clientを使って独自に実装していましたが、実際はLaravel公式のOAuth認証パッケージLaravel Socialiteを使いたいところです。

Laravel Socialiteは非常に多くの認証プロバイダーに対応しています。対応プロバイダーは、Socialite Providersで確認できます。
Microsoft Graphについてももちろん対応しています。

この記事では、Laravel Socialiteを利用してAzure ADのアカウントで認証を実装する手順について確認します。
Auth Scaffoldは使わず、つまりデータベース認証は使わずに、OAuth認証後にLaravel標準のAuthユーザとしてログインし、Auth middlewareを使って要認証のルーティングを保護する手順についても説明しています。

この記事に含まないこと

Azure ADに登録されたアカウントによる認証にのみフォーカスして説明しています。
Microsoft Graph APIを利用したデータの取得については説明しません。
よって、アクセストークンの更新についても言及していません。

Laravelプロジェクトの前提条件

必須ではないですが、Laravel Homestead上でプロジェクトを動かしています。
ホスト名はデフォルトの homestead.test です。

Laravel Homestead環境のセットアップについては、「2019版 Laravel Homestead セットアップからhttpsによるアクセス手順まで」を参照ください。

Azure ADへアプリケーションを登録

前回の記事の「Azure Active Directory 管理センターでアプリを登録する」の手順に従って、アプリケーションを作成してください。

homestead.testで動作を確認する場合は、リダイレクトURIにhttps://homestead.test/authorizeを追加してください。

socialiteproviders/microsoft-graphのインストール

Laravelプロジェクトに、Microsoft Graph用Socialiteパッケージsocialiteproviders/microsoft-graphをインストールします。
Socialite自体は依存関係でインストールされるので明示的にインストールする必要はありません。

$ composer require socialiteproviders/microsoft-graph

providers\SocialiteProviders\Manager\ServiceProvider::classを追加します。

    'providers' => [
        ...
        \SocialiteProviders\Manager\ServiceProvider::class, 
    ],

続いて、イベント\SocialiteProviders\Manager\SocialiteWasCalled::classをリスナーSocialiteProviders\\Graph\\GraphExtendSocialite@handleを指定して追加します。

// App/Providers/EventServiceProvider
<?php

namespace App\Providers;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],

        \SocialiteProviders\Manager\SocialiteWasCalled::class => [
            // add your listeners (aka providers) here
            'SocialiteProviders\\Graph\\GraphExtendSocialite@handle',
        ],
    ];
    // 略
}

アプリケーションID・シークレット・リダイレクトURIを設定

config/services.phpに、以下の設定を追加します。

    'graph' => [
        'client_id' => env('GRAPH_KEY'),
        'client_secret' => env('GRAPH_SECRET'),
        'redirect' => env('GRAPH_REDIRECT_URI')
    ],

.envにアプリケーションID(クライアントID)、シークレット、リダイレクトURIを追加します。

GRAPH_KEY=xxxxxxxxx-0000-0000-0000-xxxxxxx
GRAPH_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXX
GRAPH_REDIRECT_URI=http://homestead.test/authorize

OAuth認証の実装

LoginControllerを生成して、認証ロジックを追加していきます。

$ php artisan make:controller LoginController
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Auth;
use Socialite;

class LoginController extends Controller
{
    /**
     * Redirect the user to the graph authentication page.
     *
     * @return \Illuminate\Http\Response
     */
    public function redirectToProvider()
    {
        return Socialite::driver('graph')->redirect();
    }

    /**
     * Obtain the user information from graph.
     *
     * @return \Illuminate\Http\Response
     */
    public function handleProviderCallback()
    {
        $socialiteUser = Socialite::driver('graph')->user();

        dd($socialiteUser);
    }
}

一旦、Socialiteのuserメソッドから取得したインスタンスのダンプを表示して、ログインしたアカウント情報を確認するようにしています。

ルーティングroutes/web.phpにコントローラLoginControllerのアクションを追加

Route::get('login', 'LoginController@redirectToProvider')->name('login');
Route::get('authorize', 'LoginController@handleProviderCallback')->name('authorize');

Laravel標準のAuthミドルウェアは、未ログイン時にはloginというルーティング名にリダイレクトするので、ルーティング名を指定しておきます。
参照: Authenticationの[Redirecting Unauthenticated Users]

ここまでで、https://homestead.test/loginをブラウザで開きMicrosoft アカウントでログインすると以下のように取得したユーザの情報を確認できます。

ユーザレコードの追加とAuth::login

LoginControllerhandleProviderCallbackに、Userモデルのレコード保存と生成して、LaravelのAuthのユーザとしてログインする処理を追加していきます。

handleProviderCallbackを以下のように書き換えます。

    public function handleProviderCallback()
    {
        $socialiteUser = Socialite::driver('graph')->user();

        $user = User::where(['email' => $socialiteUser->getEmail()])->first();

        // 新規ユーザの場合は、レコードを追加
        if(!$user){
            $user = new User;
            $user->name = $socialiteUser->getName();
            $user->email = $socialiteUser->getEmail();
            $user->save();
        }
        Auth::login($user);
        return redirect()->route('/');
    }

マイグレーションを実行して、Userモデル用のテーブルを追加しておきます。

パスワードは不要なので、とりあえず2014_10_12_000000_create_users_table.phpを開きnullableにしておきます。

    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password')->nullable();
            $table->rememberToken();
            $table->timestamps();
        });
    }

マイグレーションを実行します。

$ artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (0.08 seconds)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (0.1 seconds)

確認用に、resources/views/welcom.blade.phpを開き以下の箇所を修正します。

Before

            @if (Route::has('login'))
                <div class="top-right links">
                    @auth
                        <a href="{{ url('/home') }}">Home</a>
                    @else
                        <a href="{{ route('login') }}">Login</a>

                        @if (Route::has('register'))
                            <a href="{{ route('register') }}">Register</a>
                        @endif
                    @endauth
                </div>
            @endif

After

            <div class="top-right links">
                @auth
                    <span>{{Auth::user()->name}}</span>
                @else
                    <a href="{{ route('login') }}">Login</a>
                @endauth
            </div>

再びhttps://homestead.test/loginをブラウザで開きMicrosoft アカウントでログインするとWelcomeページにリダイレクトされます。
右上にログインしたユーザが表示されています。

データベースのusersテーブルには、以下の通りレコードが追加されています。

mysql> select * from users;
+----+---------------+----------------------------------+-------------------+----------+----------------+---------------------+---------------------+
| id | name          | email                            | email_verified_at | password | remember_token | created_at          | updated_at          |
+----+---------------+----------------------------------+-------------------+----------+----------------+---------------------+---------------------+
|  1 | 鈴木 一郎     | suzuki@o365trial.onmicrosoft.com | NULL              | NULL     | NULL           | 2019-08-13 05:46:50 | 2019-08-13 05:46:50 |
+----+---------------+----------------------------------+-------------------+----------+----------------+---------------------+---------------------+
1 row in set (0.00 sec)

auth middlewareの設定

パス / を要認証にしてみます。routes/web.phpを以下のように書き換えます。
通常は、要認証のルーティングは複数あるはずはので、以下は1つですがRoute::groupを使っています。

Route::group(['middleware' => 'auth'], function() {
    Route::get('/', function () {
        return view('welcome');
    });
});

未認証でhttps://homestead.testにアクセスすると、自動的にMicrosoftアカウントの認証ページにリダイレクトするようになります。

シングルテナントモードへの対応

アプリケーションを新規作成時に[サポートされているアカウントの種類]で「この組織ディレクトリのみに含まれるアカウント (株式会社XXXXX のみ – シングル テナント)」を選択した場合、認証エンドポイントはテナントIdを指定したURL「https://login.microsoftonline.com/<テナントId>/oauth2/v2.0/authorize」になります。つまり「https://login.microsoftonline.com/common/oauth2/v2.0/authorize」は使えません。

シングルテナントモードの場合は、Using a Custom Tenant Idに記載されている通りsetTenantIdでテナントIdをセットすればOKです。

この記事のLoginController.phpの場合は以下のように修正します。

class LoginController extends Controller
{
    private $tenantId;

    public function __construct(){
        $this->tenantId = env('ADD_TENANT_ID');
    }

    /**
     * Redirect the user to the graph authentication page.
     *
     * @return \Illuminate\Http\Response
     */
    public function redirectToProvider()
    {
        return Socialite::driver('graph')->setTenantId($tenantId)->redirect();
    }

    /**
     * Obtain the user information from graph.
     *
     * @return \Illuminate\Http\Response
     */
    public function handleProviderCallback()
    {
        $socialiteUser = Socialite::driver('graph')->setTenantId($tenantId)->user();

        // 中略

        return redirect('/');
    }
}