Laravel 5 でOpenAMのSAML認証に対応する

OpenAMと連携し、Laravelで作成したWebアプリケーションをSAML2認証に対応させる方法です。

下記のサンプルを作った際のリポジトリをgithubで公開しています。
→ https://github.com/tatsuyaueda/LaravelSAML2_Sample

画像のサイズが大きすぎるのは、気が向いたら直します。

# Laravelの環境を準備する

### Laravelのプロジェクトを作成する
`composer create-project laravel/laravel LaravelSAML2 --prefer-dist`

### プロジェクトのディレクトリに移動する
`cd LaravelSAML2`

### SAML2のモジュールを追加する
`composer require aacotroneo/laravel-saml2`

### Laravelで認証を有効にする
`php artisan make:auth`

### .envファイルを編集し、データベースに接続出来るようにする
MySQLの場合はこんな感じ

```
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=foobar
DB_USERNAME=foobar
DB_PASSWORD=foobar
```

### 認証関係のテーブルをマイグレーションする
`php artisan migrate`

ここで動作確認用のユーザを登録し、そのユーザでログインできることを確認します。

### 設定ファイルの編集

#### config/app.php

```
'providers' => [
        ...
        Aacotroneo\Saml2\Saml2ServiceProvider::class,
]

'alias' => [
        ...
        'Saml2' => Aacotroneo\Saml2\Facades\Saml2Auth::class,
]
```

### config/saml2_settings.php

下記のコマンドでテンプレートが自動生成されます。
`php artisan vendor:publish`

entityId は OpenAM と合わせる必要があります。
(違っていると、LaravelのログでSaml2 error ["invalid_response"]が出力されます。)

```
<?php

return $settings = array( 'useRoutes' => true,
    'routesPrefix' => '/saml2',
    'routesMiddleware' => ['saml'],
    'retrieveParametersFromServer' => false,
    'logoutRoute' => '/logout',
    'loginRoute' => '/home',
    'errorRoute' => '/error',
    'strict' => true, //@todo: make this depend on laravel config
    'debug' => true, //@todo: make this depend on laravel config
    'sp' => array(
        'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
        'x509cert' => 'foobar',
        'privateKey' => 'foobar',
    ),
    'idp' => array(
        'entityId' => 'https://openam.example.com/OpenAM',
        'singleSignOnService' => array(
            'url' => 'https://openam.example.com/OpenAM/SSORedirect/metaAlias/idp',
        ),
        'singleLogoutService' => array(
            'url' => 'https://openam.example.com/OpenAM/IDPSloRedirect/metaAlias/idp',
        ),
        'certFingerprint' => 'foobar',
    ),
    'security' => array(
        'nameIdEncrypted' => false,
        'authnRequestsSigned' => true,
        'logoutRequestSigned' => false,
        'logoutResponseSigned' => false,
        'signMetadata' => false,
        'wantMessagesSigned' => false,
        'wantAssertionsSigned' => false,
        'wantNameIdEncrypted' => false,
        'requestedAuthnContext' => true,
    ),
);
```

#### app/Http/Kernel.php

```
protected $middlewareGroups = [
        ...
        'saml' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];
```

#### app/Providers/EventServiceProvider.php

```
    use App\User;

    public function boot() {
        ....

        Event::listen('Aacotroneo\Saml2\Events\Saml2LoginEvent', function ($event) {

            $user = $event->getSaml2User();
            $userData = [
                'id' => $user->getUserId(),
                'attributes' => $user->getAttributes(),
                'assertion' => $user->getRawSamlAssertion()
            ];

            $laravelUser = User::where('email', $user->getUserId())
                    ->first();

            Auth::login($laravelUser);
        });

        Event::listen('Aacotroneo\Saml2\Events\Saml2LogoutEvent', function ($event) {
            //Auth::logout();
            //Session::save();
        });
    }
```

### 動作確認
/saml2/login にアクセスすると、SAMLで認証が行われ、認証に成功すると /home、失敗すると /error にリダイレクトされます。
ログインページなどに、このURLへのリンクを作成すると便利だと思います。

# OpenAMの設定

下記の操作はすでに、トラストサークルが作成されている状態から進めます。

### エンティティーの作成

OpenAMの管理画面にログインし、「Federation」をクリックします。
![001.png](https://qiita-image-store.s3.amazonaws.com/0/25028/c4895f47-68f9-bb3a-b5ef-8abd185b8a6f.png)

エンティティープロバイダ」から「エンティティーのインポート」をクリックします。
![002.png](https://qiita-image-store.s3.amazonaws.com/0/25028/fde0549e-784a-ee6b-70c7-60190c0d1adf.png)

メタデータファイルの場所をURLもしくは、ファイルで指定し、「了解」をクリックします。
Webブラウザで /saml2/metadata にアクセスした際に表示される内容がメタデータとなります。

![003.png](https://qiita-image-store.s3.amazonaws.com/0/25028/cbe0feb1-ccb4-7c94-e586-e82497e18c6b.png)

すでに作成されているトラストサークルの名前をクリックします。下記の場合、「SimpleSAML」となります。

![004.png](https://qiita-image-store.s3.amazonaws.com/0/25028/b86904ac-5cf6-0f9a-da40-72a5e9949ca5.png)

「エンティティープロバイダ」の「選択可能」に上記でインポートしたエンティティープロバイダが登録されているので、
それを選択し、「追加」をクリックします。続けて、右上の「了解」をクリックします。

![005.png](https://qiita-image-store.s3.amazonaws.com/0/25028/c420ac16-6d49-a4d9-7ae2-c15e75819f65.png)


ここまでの作業でOpenAM側の設定が完了です。

# laravel-saml2 0.11系から2.10系にバージョンアップする

laravel-saml2がSAMLライブラリとして利用しているonelogin/php-samlは2.0系までphp-mcryptに依存しています。
[php-mcryptはPHP 7.1.0で非推奨に、PHP 7.2.0で廃止されてしまいました。](https://www.php.net/manual/ja/migration71.deprecated.php)

- こんな感じ
    - laravel-saml2 0.11系→php-saml 2.0系→php-mcrypt→PHP 7.1系
    - laravel-saml2 2系→php-saml 3.0系→php-cryptへの依存無し

たいした変更じゃないですが、laravel-saml2 2.0系では複数のIDPに対応した関係で、設定ファイルの構成が変更となっているため、バージョンアップ時のmemoです。

## laravel-saml2のバージョンアップ

```composer.json
"require": {
        "aacotroneo/laravel-saml2": "^2.0",
}
```

```
# composer php update aacotroneo/laravel-saml2
```

## 設定ファイルの更新

### config/saml2_settings.php

idpNamesの連想配列を追加し、strict/debug/sp/idp/securityのをそっくり削除
削除した部分は次の設定ファイルに移動します。

```config/saml2_settings.php
    // ADD
    // laravel-saml2 2.0系から複数のIDPを設定出来るようになったため、その識別子
    'idpNames' => ['kcprd'],

    // DELETE
    'strict' => true, //@todo: make this depend on laravel config
    'debug' => env('APP_DEBUG'),
    'sp' => array(
        ....
    ),
    'idp' => array(
        ....
    ),
    'security' => array(
        ....
    ),
```

### config/saml2/kcprd_idp_settings.php

ファイル名の kcprd はsaml2_settings.phpで設定した idpNames と合わせます。
$this_idp_env_id は環境変数からパラメタを定義する際の識別子で、idpNamesと合わせるのが良いかと思います。

基本的にOnelogin samlのファイルそのままです。
$settingsの中身はsaml2_settings.phpで削除した部分をそのまま入れれば大丈夫だと思います。

```config/saml2/kcprd_idp_settings.php
<?php
// If you choose to use ENV vars to define these values, give this IdP its own env var names
// so you can define different values for each IdP, all starting with 'SAML2_'.$this_idp_env_id
$this_idp_env_id = 'KCPRD';

//This is variable is for simplesaml example only.
// For real IdP, you must set the url values in the 'idp' config to conform to the IdP's real urls.
return $settings = array(
    /*****
     * One Login Settings
     */
    'debug' => env('APP_DEBUG', false),
    'sp' => array(
        'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
        'x509cert' => env('SAML2_'.$this_idp_env_id.'_SP_x509',''),
        'privateKey' => env('SAML2_'.$this_idp_env_id.'_SP_PRIVATEKEY',''),
        'entityId' => env('SAML2_'.$this_idp_env_id.'_SP_ENTITYID',''),
        'assertionConsumerService' => array(
            'url' => '',
        ),
        'singleLogoutService' => array(
            'url' => '',
        ),
    ),
    'idp' => array(
        'entityId' => env('SAML2_'.$this_idp_env_id.'_IDP_ENTITYID', $idp_host . '/saml2/idp/metadata.php'),
        'singleSignOnService' => array(
            'url' => env('SAML2_'.$this_idp_env_id.'_IDP_SSO_URL', $idp_host . '/saml2/idp/SSOService.php'),
        ),
        'singleLogoutService' => array(
            'url' => env('SAML2_'.$this_idp_env_id.'_IDP_SL_URL', $idp_host . '/saml2/idp/SingleLogoutService.php'),
        ),
        'x509cert' => env('SAML2_'.$this_idp_env_id.'_IDP_x509', ''),
        /*
         *  Instead of use the whole x509cert you can use a fingerprint
         *  (openssl x509 -noout -fingerprint -in "idp.crt" to generate it)
         */
    ),
    /***
     *  OneLogin advanced settings
     */
    'security' => array(
        'nameIdEncrypted' => false,
        'authnRequestsSigned' => false,
        'logoutRequestSigned' => false,
        'logoutResponseSigned' => false,
        'signMetadata' => false,
        'wantMessagesSigned' => false,
        'wantAssertionsSigned' => false,
        'wantNameIdEncrypted' => false,
        'requestedAuthnContext' => true,
    ),
    'contactPerson' => array(
        'technical' => array(
            'givenName' => 'name',
            'emailAddress' => 'no@reply.com'
        ),
        'support' => array(
            'givenName' => 'Support',
            'emailAddress' => 'no@reply.com'
        ),
    ),
    'organization' => array(
        'en-US' => array(
            'name' => 'Name',
            'displayname' => 'Display Name',
        ),
    ),
   'wantAssertionsSigned' => true,
   'wantNameIdEncrypted' => false,
);
```

## SAML2ログイン時のURL

今までは /saml2/login でしたが、 /saml2/[idpName]/login となります。
上の設定ファイルでは、 /saml2/kcprd/login となります。