LaravelアプリケーションにSAML認証方式のシングルサインオンログインを実装する(IdP設定+ログイン処理編)

これは[株式会社マイホム アドベントカレンダー2022](https://qiita.com/advent-calendar/2022/myhm)の18日目の記事です。
記事の内容は会社の業務とは一切関係ありません。

2024.08 追記
記事投稿当時 Microsoft Azure Active Directory の名称だったサービスは、現在 [Microsoft Entra IDに名称が変わっています。](https://learn.microsoft.com/ja-jp/entra/fundamentals/new-name)
以下の記事内ではサービス名を読み替えてください。

## 概要


[前回の記事](https://qiita.com/shkfn/items/8ebea0d0dd605b955221)の続きです。

## IdPにSPの情報を設定する

### SAMLアプリケーションを登録する

Azureコンソール > Azure Active Directory > エンタープライズアプリケーション > 新しいアプリケーション > 独自のアプリケーションの作成

任意のアプリ名をつけて、ギャラリーに見つからない…を選んで作成します。
![2022-12-14_22-32-07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260612/39f7cf14-c92a-7cba-6bfc-71ec4c8aee01.png)

### SAMLアプリケーションの設定

シングルサインオンを使えるようにするだけなら、ユーザー割り当てとシングルサインオンの設定だけです。
ユーザー割り当てについては操作を迷うようなところは無いと思うので省略します。
![2022-12-14_22-47-05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260612/20c33ea0-3ee7-6ad0-d4eb-f7459b4cf83f.png)

#### シングルサインオンの設定

##### 基本的なSAML構成

![2022-12-15_00-49-48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260612/c267be20-a78a-709b-d0b9-d9b114319128.png)

[前回の記事](https://qiita.com/shkfn/items/8ebea0d0dd605b955221)で最後に出力した`Entity ID`と`Assertion Consumer Service URL`を設定します。
![2022-12-18_02-24-12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260612/9318196b-5906-5374-36b1-20b6cf7486db.png)

##### 属性とクレーム

![2022-12-18_15-22-20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260612/8b71a936-2136-c574-802e-a12fccf7c47f.png)
これはIdPでサインオンしたユーザー情報は何の項目をどのような名称で送るかというマッピングの設定です。
IdPでサインオンしたユーザーがSPのユーザーの誰に該当するのか(SPでログインさせるユーザーは誰か)を特定するため、IdPとSPで共通して持つ一意に識別可能な値を送ってもらう必要があります。
多くの場合ログインIDとしてメールアドレスが採用されているケースが多いと思いますので、この記事でもメールアドレスを採用します。
デフォルトでこれだけマッピングされていますので、カスタマイズの必要がなければ特に設定しなくても構いません。
（AzureADでユーザー登録する際、ユーザープリンシパル名をメールアドレスにしているケースも多いと思いますが、各自の都合に合わせて追加、変更してください）
![2022-12-18_15-36-04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260612/57cf53b6-106a-8c56-12f5-f7bfe05d1507.png)

##### SAML 証明書

証明書 (Base64)をダウンロードしておきます。
![2022-12-18_16-00-25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260612/66397be8-81c5-7fa3-b8a6-0e3503bf55d5.png)

##### SAMLアプリケーション のセットアップ

SP側でテナント登録を完了させるのに必要な情報が提示されていますので、コピーしておきます。
![2022-12-18_16-06-50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260612/eb7de8c1-e31c-8eb8-b4d6-cfedfc33cd25.png)

## SP側でテナント登録を完了させる

IdPの設定で取得した情報を使って、本来テナント登録に必要だった項目を補完します。

```shell
php artisan saml2:update-tenant 1 \
  --entityId="https://sts.windows.net/xxxxx-xxxxx-xxxxx/" \
  --loginUrl="https://login.microsoftonline.com/xxxxx-xxxxx-xxxxx/saml2" \
  --logoutUrl="https://login.microsoftonline.com/xxxxx-xxxxx-xxxxx/saml2" \
  --x509cert="-----BEGIN CERTIFICATE-----
xxxxxxxxxxxxxxxx
-----END CERTIFICATE-----
"
```

entityIdオプション=Azure AD 識別子で、その他はオプション名そのままなので迷うことはないでしょう。
（updateコマンドも例によって値をそのままDBに入れているだけなので、DBツール使って値をコピペで更新してしまっても大丈夫です。）

## SPのログイン処理を実装

ライブラリのREADMEにあるサンプルコードをベースにログイン処理を実装します。
イベントリスナーとして動作させるコードなので、Laravelサービスプロバイダのbootメソッドなど、フレームワーク起動時に必ず実行される箇所に書かれている必要があります。
処理内容的に今回は`AuthServiceProvider`のbootメソッドに書いてみました。

```php:app/Providers/AuthServiceProvider.php
<?php

namespace App\Providers;

use Auth;
use Event;
use App\Models\User;

class AuthServiceProvider extends ServiceProvider
{
    public function boot()
    {
        Event::listen(\Slides\Saml2\Events\SignedIn::class, function (\Slides\Saml2\Events\SignedIn $event) {
            $messageId = $event->getAuth()->getLastMessageId();
            
            // your own code preventing reuse of a $messageId to stop replay attacks
            // コメントにあるようにリプレイ攻撃の対策コードを書きましょう。$messageIdはSAMLトークン毎にユニークなので、同じ$messageIdが再送されてくるということ自体が異常と見做せます。
            $samlUser = $event->getSaml2User();
            
            $userData = [
                'id' => $samlUser->getUserId(),
                'attributes' => $samlUser->getAttributes(), // 属性とクレームでマップしたユーザーデータが連想配列としてデコードされています
                'assertion' => $samlUser->getRawSamlAssertion() // 送られたXMLのbase64エンコードデータです
            ];
            // 送られてきたユーザーデータからサービスとして保持するユーザーデータを引き当てます。
            $user = User::where('email', $userData['attributes']['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'][0])->firstOrFail();
            
            // Login a user.
            Auth::login($user);
        });
    }
}
```

## シングルサインオンをテストする

以上で必要な準備は完了していますので、シングルサインオンを使うことができます。
スタート地点になるのは`https://localhost/saml2/{uuid}/login`です。
まずこのURLにブラウザアクセスすることで、IdPのログイン画面へリダイレクトされます。その後ログインの手続きを進めて行き、ここまでの設定が正しく行われていれば、ログイン成功後`https://localhost/dashboard`へと到達できるはずです。