Laravel 5 でSAMLシングルサインオンする

LaravelでSAMLでのシングルサインオンに対応した際のメモです。
ほぼlaravel-saml2のREADMEと同内容です。

インストールについては@tatsuya_infoさんの下記記事が詳しいので、そちらも是非ご参考ください。
[Laravel 5 でOpenAMのSAML認証に対応する](
https://qiita.com/tatsuya_info/items/51b6af5ce9159234d1df)

## 環境

Laravel 5.5、5.4で動作を確認しています。

## インストール

### laravel-saml2をインストールする

```cmd
composer require aacotroneo/laravel-saml2
```

Laravel 5.5 より前の場合 `config\app.php` にサービスプロバイダを追加します。

```php:config\app.php

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

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

vendor:publishする

```cmd
php artisan vendor:publish --provider="Aacotroneo\Saml2\Saml2ServiceProvider"
```

app\config\saml2_settings.phpが作成されます。

### Laravelの認証を作成

下記コマンドでLaravelのAuthを作成します。

```cmd
php artisan make:auth
php artisan migrate
```

## SAML設定

### SPメタデータをIdPに登録

Laravel側（SAML SP）のSAMLメタデータを下記URLから確認し、IdP側に登録します。

`http://localhost/Laravel-app-name/public/saml2/metadata`

### Laravel側にIdPメタデータを設定
Laravel側の `saml2_settings.php` にIdPメタデータを設定する。
各環境に合わせてentityId、singleSignOnService、singleLogoutService、x509certを指定します。
ここではGoogleをIdPとして利用しています。

```php:app\config\saml2_settings.php
// Identity Provider Data that we want connect with our SP
    'idp' => array(
        // Identifier of the IdP entity  (must be a URI)
        'entityId' => 'https://accounts.google.com/o/saml2?idpid=****',
        // SSO endpoint info of the IdP. (Authentication Request protocol)
        'singleSignOnService' => array(
            // URL Target of the IdP where the SP will send the Authentication Request Message,
            // using HTTP-Redirect binding.
            'url' => 'https://accounts.google.com/o/saml2/idp?idpid=****',

        ),
        // SLO endpoint info of the IdP.
        'singleLogoutService' => array(
            // URL Location of the IdP where the SP will send the SLO Request,
            // using HTTP-Redirect binding.
            'url' => 'https://accounts.google.com/Logout',
        ),
        // Public x509 certificate of the IdP
        'x509cert' => '****',
        /*
         *  Instead of use the whole x509cert you can use a fingerprint
         *  (openssl x509 -noout -fingerprint -in "idp.crt" to generate it)
         */
        // 'certFingerprint' => '',
```

### routesMiddlewareの変更

VerifyCsrfTokenを入れるとログインセッションが作られなかったため、
`saml2_settings.php` のroutesMiddlewareを以下のように変更します。

```php:app\config\saml2_settings.php
'routesMiddleware' => ['saml'],
```

`Kernel.php` に新規ミドルウェアグループsamlを追加します。
samlグループの内容はwebグループからVerifyCsrfTokenを抜いたものです。

```php:app\Http\Kernel.php
protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            'throttle:60,1',
            'bindings',
        ],

        // samlグループを追加
        '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,
        ],
    ];
```

## 認証部分の作成

### SAMLログイン・ログアウトイベントリスナの作成

SAMLログイン・ログアウトしたときのイベントを記述します。
LaravelのAuthと連携させるため

* SAMLログイン時にAuthでもログイン
* SAMLログアウト時にAuthでもログアウト

するようにします。

`EventServiceProvider.php` のbootメソッドに下記を追記します。

```php:app\Providers\EventServiceProvider.php
 Event::listen('Aacotroneo\Saml2\Events\Saml2LoginEvent', function (Saml2LoginEvent $event) {
            $messageId = $event->getSaml2Auth()->getLastMessageId();
        // your own code preventing reuse of a $messageId to stop replay attacks
        $user = $event->getSaml2User();

        // 属性からUserモデルを取得する
        $userData = [
            'id' => $user->getUserId(),
            'attributes' => $user->getAttributes(),
            'assertion' => $user->getRawSamlAssertion()
        ];
        $laravelUser = \App\User::where('email', $userData['attributes']['emailAddress'])->first();

        //if it does not exist create it and go on  or show an error message
        if ($laravelUser) {
            Auth::login($laravelUser);
        } else {
            abort(401, 'Authorization Required');
        }
        });

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

```

取得する属性は環境に合わせてください。
また、ここでは認証エラー時に401エラーとしています、401エラーに対応するビューを下記パスに作っておきます。
`app\resources\views\errors\401.blade.php`

### SAML認証Middlewareの作成

SAML認証するミドルウェアを作成します。
このミドルウェアを適用することで、SAMLログインしないとページが見られなくなります。

artisanコマンドでSamlAuthという名前でミドルウェアを作成します。

```cmd
php artisan make:middleware SamlAuth
```

`SamlAuth.php`を編集し以下のようにします。

```php:app\Http\Middleware\SamlAuth.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\URL;
use Aacotroneo\Saml2\Facades\Saml2Auth;

class SamlAuth
{

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {

        // SAMLログイン認証
        if (Auth::guest()) {
            if ($request->ajax()) {
                return response('Unauthorized.', 401);
            } else {
                Saml2Auth::login(URL::full());
            }
        }
        return $next($request);
    }

}
```

`Kernel.php` のrouteMiddlewareにSamlAuthを登録します。

```php:app\Http\Kernel.php
    protected $routeMiddleware = [
        'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        // 追加
        'samlauth' => \App\Http\Middleware\SamlAuth::class,
    ];
```

## 認証を適用する

認証ミドルウェアを適用します。

下記はミドルウェアをルートグループに適用した例です。
適用の仕方は色々ありますが、私はこの方法が気に入っています。

```php
Route::middleware(['samlauth'])->group(function () {
    Route::get('/home', 'HomeController@index');
});
```

## 動作確認

あらかじめUsersテーブルにIdP側と共通のキー（この例ではメールアドレス）を持つユーザーを作っておきます。

/homeにアクセスすると、IdPのログイン画面が表示されます。
ログインするとメールアドレスをキーにシングルサインオンできるようになります。

## 参考資料

[Laravel 5 でOpenAMのSAML認証に対応する](https://qiita.com/tatsuya_info/items/51b6af5ce9159234d1df)

<https://github.com/aacotroneo/laravel-saml2>

<https://github.com/taiyeoguns/laravel-saml-sp-demo>