LoginSignup
8
3

More than 1 year has passed since last update.

LaravelアプリケーションにSAML認証方式のシングルサインオンログインを実装する(ライブラリ編)

Last updated at Posted at 2022-12-17

これは株式会社マイホム アドベントカレンダー2022の17日目の記事です。
記事の内容は会社の業務とは一切関係ありません。

概要

LaravelアプリケーションにSAML認証方式によるシングルサインオンでログインする実装例です。

前提

  • Laravel及びスターターキットのLaravel Breezeをインストール済みで、ログイン可能なユーザーも登録済みであること。https://localhost/にブラウザアクセス可能な状態になっていること。
  • Laravel Breezeのセットアップ等については公式、有志邦訳ドキュメントをはじめとして参考例が多数ありますので省略します。
  • Identity Provider(IdP)にはMicrosoft Azure ActiveDirectory(AzureAD)を使用します。
    • 本記事のIdP側の解説ベースはAzureADですが、SAML認証方式で扱う情報は標準化されていますので、Google Workspace(旧G Suite)を使用しても設定する内容は基本的に変わりません。

手順

OneLogin toolkitをベースに作られた24Slides/laravel-saml2というライブラリを使用します。

1. インストール

composer require 24slides/laravel-saml2

2. configファイル展開とミドルウェア設定

php artisan vendor:publish --provider="Slides\Saml2\ServiceProvider"

config/saml2.phpファイルが作成されます。
ライブラリのREADMEとcofigファイル内にも説明がかかれていますが、コアな部分を簡単に解説します。

config/saml2.php
return [

    /*
    |--------------------------------------------------------------------------
    | Tenant Model
    |--------------------------------------------------------------------------
    |
    | ここを変更することでテナントモデルを独自のものにオーバーライドできます
    |
    */

    'tenantModel' => \Slides\Saml2\Models\Tenant::class,

    /*
    |--------------------------------------------------------------------------
    | Use built-in routes
    |--------------------------------------------------------------------------
    |
    | trueに設定するとライブラリから提供されるビルトインルートです。
    |
    | Method | URI                               | Name
    | -------|-----------------------------------|------------------
    | POST   | {routesPrefix}/{uuid}/acs       | saml.acs
    | GET    | {routesPrefix}/{uuid}/login     | saml.login
    | GET    | {routesPrefix}/{uuid}/logout    | saml.logout
    | GET    | {routesPrefix}/{uuid}/metadata  | saml.metadata
    | GET    | {routesPrefix}/{uuid}/sls       | saml.sls
    |
    */

    'useRoutes' => true,

    /*
    |--------------------------------------------------------------------------
    | Built-in routes prefix
    |--------------------------------------------------------------------------
    |
    | ライブラリのビルトインルートで使用するプレフィックス。
    |
    */

    'routesPrefix' => '/saml2',

ほぼコメントのままですが、展開された初期状態でライブラリからビルトインのルートが提供されるようになっています。
上記の設定ならhttps://localhost/saml2/{uuid}/acsのような感じです。
コントローラをカスタマイズしたいなどの場合は、ビルトインルートをやめて独自に定義することもできます。
ただし、デフォルトのコントローラの動作を変える場合、脆弱性を作り込まないように注意する必要があります。よくわからない場合は無闇に改造しないようにしましょう。

使い始めるにあたってもう一つ重要なオプションが下記のビルトインルートにかかるミドルウェアグループの指定です。
初期状態では[]と未指定になっていますので、以下のように変更します。

config/saml2.php
'routesMiddleware' => ['saml'],

次にREADMEを参考にapp/Http/Kernel.phpでミドルウェアグループを追加定義します。
グループ名はconfigで指定した名称と合わせます。

app/Http/Kernel.php
    protected $middlewareGroups = [
        // 省略

        'saml' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
        ],
    ];

最低限必要なミドルウェアはStartSessionですが、アプリケーションの都合で追加しても大丈夫です。
ただし、VerifyCsrfTokenミドルウェアは追加しないようにします。
大雑把にいうとSAMLは意図的にCSRFさせているようなものでもあるため、このミドルウェアで保護すると動作を阻害してしまいます。

その他多くのオプションをOneLogin Toolkitから引き継いでいますので、必要に応じてそちらも確認してください。

3. マイグレーション

php artisan migrate

ライブラリのマイグレーションからsaml2_tenantsテーブルが作成されます。

この後の作業にあたっての注意点

こちらのライブラリの使用にあたり、困りごとが一つ。
ライブラリではIdPをテナントという単位で管理するのですが、ライブラリが提供するテナント登録コマンドsaml2:create-tenantではいくつかの必須オプションがあり、ブランクも入力不可となるように制限されています。
しかもそれらの値はIdPの設定を進めないと登録すべき値が取得できません。
ところが!
IdPで設定を進めようにも、こちらはこちらでSPでテナント登録を行うと取得できる値を要求してきます。(IdPがGSuiteでも同じような状況です)

2022-12-14_16-58-26.png

初手からデッドロック状態なので登録コマンドを自作してしまいましょう。
(コマンド作るの面倒という方はIdP側の必須項目を仮の値で進めて、テナント登録に必要な証明書などを先に取得してしまうという手順で回避もする方法もあります)

テナント登録コマンドの作成と実行

オリジナルのコマンドはvendor/24slides/laravel-saml2/src/Commands/CreateTenant.phpで内容を見てみると、やっていることはuuidの生成と値の整形程度でほぼDB登録するだけのお仕事です。自作といっても簡単な改造程度です。

php artisan make:command CreateTenant
app/Console/Commands/CreateTenant.php
<?php

namespace App\Console\Commands;

use Slides\Saml2\Commands\CreateTenant as Origin;
use Slides\Saml2\Helpers\ConsoleHelper;

class CreateTenant extends Origin
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'saml2:create-tenant-custom
                            { --k|key= : A tenant custom key }
                            { --entityId= : IdP Issuer URL }
                            { --loginUrl= : IdP Sign on URL }
                            { --logoutUrl= : IdP Logout URL }
                            { --relayStateUrl= : Redirection URL after successful login }
                            { --nameIdFormat= : Name ID Format ("persistent" by default) }
                            { --x509cert= : x509 certificate (base64) }
                            { --metadata= : A custom metadata }';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'テナント登録を行い、Entity ID、各種ルートを表示する';


    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        // DBの制約でnullが許可されていないので空文字を初期値に設定
        $entityId = $this->option('entityId') ?? '';
        $loginUrl = $this->option('loginUrl') ?? '';
        $logoutUrl = $this->option('logoutUrl') ?? '';
        $x509cert = $this->option('x509cert') ?? '';
        $key = $this->option('key'); // null OK
        $metadata = ConsoleHelper::stringToArray($this->option('metadata'));

        if($key && ($tenant = $this->tenants->findByKey($key))) {
            $this->renderTenants($tenant, 'Already found tenant(s) using this key');
            $this->error(
                'Cannot create a tenant because the key is already being associated with other tenants.'
                . PHP_EOL . 'Firstly, delete tenant(s) or try to create with another with another key.'
            );

            return 0;
        }

        $tenant = new \Slides\Saml2\Models\Tenant([
            'key' => $key,
            'uuid' => \Ramsey\Uuid\Uuid::uuid4(),
            'idp_entity_id' => $entityId,
            'idp_login_url' => $loginUrl,
            'idp_logout_url' => $logoutUrl,
            'idp_x509_cert' => $x509cert,
            'relay_state_url' => $this->option('relayStateUrl'),
            'name_id_format' => $this->resolveNameIdFormat(),
            'metadata' => $metadata,
        ]);

        if(!$tenant->save()) {
            $this->error('Tenant cannot be saved.');
            return 0;
        }

        $this->info("The tenant #{$tenant->id} ({$tenant->uuid}) was successfully created.");

        $this->renderTenantCredentials($tenant);

        $this->output->newLine();
        return 0;
    }
}

コマンド実行します。
relayStateUrlオプションは省略できますが、この時点でログイン成功後のリダイレクト先が決まっているのであれば指定しましょう。
(メモ代わりなのでkeyオプションはそもそも無くてもOKです)

php artisan saml2:create-tenant-custom --relayStateUrl="/dashboard" --key=azure

するとこのようにIdPに設定するための情報を取得することができます。

2022-12-18_00-21-37.png

長くなるのでこの記事は一旦ここまでで区切ります。
次回の記事でIdPの設定について解説していきます。

8
3
0

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
  3. You can use dark theme
What you can do with signing up
8
3