This tutorial describes how to implement user authentication and authorization (simply "auth" from now on) on a Laravel application via an external service. This auth paradigm is usually know as Single sign-on (SSO), its purpose is centralizing all auth management into a dedicated system.
Security Assertion Markup Language (SAML) auth is one of the several SSO implementations commonly used, based on an open data exchange format.
Some terms:
- IdP (Identity Provider): application providing auth service
- SP (Service Provider): application subscribed to use an IdP's auth service
Laravel 5.2 version has been chosen, since it's the first version implementing a standard middleware group by default (see Laravel 5.2's release notes).
tutorial-scoped assumption :
-
the application
- is developed on a UNIX-like environment (tested on Ubuntu 14.04 64 bit)
- resides inside a folder named sp.example.com
- is reachable on a web server at URL http://sp.example.com
-
an IdP is available and reachable at URL http://idp.example.com
-
create the SSL public certificate and related key, if not already available, and store them together with IdP's public certificate (e.g. idp.example.com.crt):
openssl req -newkey rsa:2048 -new -x509 -days 3652 -nodes \ -out /path/to/certificate_folder/sp.example.com.crt \ -keyout /path/to/certificate_folder/sp.example.com.pem cp /path/to/idp.example.com.crt /path/to/certificate_folder
-
create Laravel application via Composer:
composer create-project --prefer-dist laravel/laravel 5.2.* sp.example.com --no-dev chmod -R 777 storage/* chmod -R 777 bootstrap/cache
tutorial-scoped assumption : development dependencies are not required
-
customize file .env as required:
APP_ENV=local APP_DEBUG=true APP_KEY=base64:jgTzCe6Iv1eYmCM57jmpzGnBeRBHfPmsGI1MXftjCAY= APP_URL=http://sp.example.com DB_HOST=my.db.host DB_DATABASE=my_database DB_USERNAME=my_username DB_PASSWORD=my_password
tutorial-scoped assumption : only the above few parameters are required
Since Laravel cannot handle authentication without a users table, create it:
-
simplify users table migration (password field is useless, with SAML authentication) by editing file database/migrations/2014_10_12_000000_create_users_table.php:
public function up() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); $table->string('email')->unique(); $table->rememberToken(); $table->timestamps(); }); }
tutorial-scoped assumption : "email" field is used as unique user identifier
-
since users are managed by the IdP, password reset table migration can be removed:
rm database/migrations/2014_10_12_100000_create_password_resets_table.php
-
run migration of customized users table:
php artisan migrate
-
install Alejandro Cotroneo's SAML library via Composer and publish its configurations to file config/saml2_settings.php:
composer require aacotroneo/laravel-saml2 --update-no-dev php artisan vendor:publish
-
add its service provider to file config/app.php:
'providers' => [ ... Aacotroneo\Saml2\Saml2ServiceProvider::class, ]
this operation sets the following list of routes:
URI Name Action Middleware / Closure web saml2/acs saml_acs Aacotroneo\Saml2\Http\Controllers\Saml2Controller@acs saml2/login saml_login Aacotroneo\Saml2\Http\Controllers\Saml2Controller@login saml2/logout saml_logout Aacotroneo\Saml2\Http\Controllers\Saml2Controller@logout saml2/metadata saml_metadata Aacotroneo\Saml2\Http\Controllers\Saml2Controller@metadata saml2/sls saml_sls Aacotroneo\Saml2\Http\Controllers\Saml2Controller@sls
The "web" middleware group above automatically starts session management, functionality strictly required to perform authentication. A slightly different middleware group is required (see below).
-
add the following custom middleware group, to avoid issues related to VerifyCsrfToken middleware, in file app/Http/Kernel.php (as suggested by SAML library's author):
protected $middlewareGroups = [ ... 'web_for_saml' => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, ], ... ];
-
customize SP and IdP metadata in file config/saml2_settings.php:
$idp_host = 'http://idp.example.com/simplesaml'; return $settings = array( ... 'routesMiddleware' => ['web_for_saml'], ... 'sp' => array( ... 'simplesaml.nameidattribute' => 'email', 'x509cert' => file_get_contents('/path/to/certificate_folder/sp.example.com.crt'), 'privateKey' => file_get_contents('/path/to/certificate_folder/sp.example.com.pem'), ... ), 'idp' => array( ... 'x509cert' => file_get_contents('/path/to/certificate_folder/idp.example.com.crt'), ), ... );
this operation customizes the list of routes as follows:
URI Name Action Middleware / Closure web saml2/acs saml_acs Aacotroneo\Saml2\Http\Controllers\Saml2Controller@acs web_for_saml saml2/login saml_login Aacotroneo\Saml2\Http\Controllers\Saml2Controller@login web_for_saml saml2/logout saml_logout Aacotroneo\Saml2\Http\Controllers\Saml2Controller@logout web_for_saml saml2/metadata saml_metadata Aacotroneo\Saml2\Http\Controllers\Saml2Controller@metadata web_for_saml saml2/sls saml_sls Aacotroneo\Saml2\Http\Controllers\Saml2Controller@sls web_for_saml -
in order to bind local authentication session to remote authentication session, add SAML login/logout event listeners into file app/Providers/EventServiceProvider.php:
public function boot(DispatcherContract $events) { parent::boot($events); $events->listen( 'Aacotroneo\Saml2\Events\Saml2LoginEvent', function (Saml2LoginEvent $event) { $user = $event->getSaml2User(); $laravelUser = User::where('email', $user->getUserId())->first(); if (empty($laravelUser)) { $laravelUser = User::create([ 'email' => $user->getUserId(), ]); } Auth::login($laravelUser); } ); $events->listen( 'Aacotroneo\Saml2\Events\Saml2LogoutEvent', function ($event) { Auth::logout(); Session::save(); } ); }
tutorial-scoped assumption : event listeners are hard-coded into file app/Providers/EventServiceProvider.php, although a better solution would consist of two classes under directory app/Listeners
-
customize welcome view, in order to easily test login and logout, by adding login/logout links to file resources/views/welcome.blade.php:
<body> <div class="container"> @if (Auth::guest()) <a href="{{ route('saml2_login') }}">Login</a> @else <a href="{{ route('saml2_logout') }}">Logout</a> @endif ... </div> </body>
-
export SP metadata in XML format, available at URL http://sp.example.com/saml2/metadata, e.g.:
<?xml version="1.0"?> <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" validUntil="2017-03-09T20:35:05Z" cacheDuration="PT604800S" entityID="http://sp.example.com/saml2/metadata"> <md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> <md:KeyDescriptor use="signing"> <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> <ds:X509Data> <ds:X509Certificate>...</ds:X509Certificate> </ds:X509Data> </ds:KeyInfo> </md:KeyDescriptor> <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://sp.example.com/saml2/sls"/> <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat> <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://sp.example.com/saml2/acs" index="1"/> </md:SPSSODescriptor> ... </md:EntityDescriptor>
-
import SP metadata to IdP: IdP's usually have easy metadata management interfaces
- https://en.wikipedia.org/wiki/Single_sign-on
- https://en.wikipedia.org/wiki/Security_Assertion_Markup_Language
- https://laravel.com/docs/5.2/releases
- https://github.com/aacotroneo/laravel-saml2
- aacotroneo/laravel-saml2#7
- https://simplesamlphp.org/docs/stable/simplesamlphp-sp
- https://laravel.com/docs/5.2/events