LaravelでSAMLでのシングルサインオンに対応した際のメモです。
ほぼlaravel-saml2のREADMEと同内容です。
インストールについては@tatsuya_infoさんの下記記事が詳しいので、そちらも是非ご参考ください。
Laravel 5 でOpenAMのSAML認証に対応する
環境
Laravel 5.5、5.4で動作を確認しています。
インストール
laravel-saml2をインストールする
composer require aacotroneo/laravel-saml2
Laravel 5.5 より前の場合 config\app.php
にサービスプロバイダを追加します。
'providers' => [
...
Aacotroneo\Saml2\Saml2ServiceProvider::class,
]
'alias' => [
...
'Saml2' => Aacotroneo\Saml2\Facades\Saml2Auth::class,
]
vendor:publishする
php artisan vendor:publish --provider="Aacotroneo\Saml2\Saml2ServiceProvider"
app\config\saml2_settings.phpが作成されます。
Laravelの認証を作成
下記コマンドでLaravelのAuthを作成します。
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として利用しています。
// 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を以下のように変更します。
'routesMiddleware' => ['saml'],
Kernel.php
に新規ミドルウェアグループsamlを追加します。
samlグループの内容はwebグループからVerifyCsrfTokenを抜いたものです。
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メソッドに下記を追記します。
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という名前でミドルウェアを作成します。
php artisan make:middleware SamlAuth
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を登録します。
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,
];
認証を適用する
認証ミドルウェアを適用します。
下記はミドルウェアをルートグループに適用した例です。
適用の仕方は色々ありますが、私はこの方法が気に入っています。
Route::middleware(['samlauth'])->group(function () {
Route::get('/home', 'HomeController@index');
});
動作確認
あらかじめUsersテーブルにIdP側と共通のキー(この例ではメールアドレス)を持つユーザーを作っておきます。
/homeにアクセスすると、IdPのログイン画面が表示されます。
ログインするとメールアドレスをキーにシングルサインオンできるようになります。