Add SAML SSO to Laravel App
This guide assumes that you have a Laravel app and want to enable SAML Single Sign-On authentication for your enterprise customers. By the end of this guide, you'll have an app that allows you to authenticate the users using SAML Single Sign-On.
Visit the GitHub repository to see the source code for the Laravel SAML SSO integration.
Integrating SAML SSO into an app involves the following steps.
- Configure SAML Single Sign-On
- Authenticate with SAML Single Sign-On
Configure SAML Single Sign-On
This step allows your tenants to configure SAML connections for their users. Read the following guides to understand more about this.
Authenticate with SAML Single Sign-On
Once you add a SAML connection, the app can use this SAML connection to initiate the SSO authentication flow using Ory Polis. The following sections focus more on the SSO authentication side.
Install Ory Polis
The first step is to deploy the Ory Polis service. Follow the deployment docs to install and configure the Ory Polis.
Setup Ory Polis
We'll use the Laravel Socialite for the integration. Socialite provides an expressive, fluent interface to OAuth authentication with external authentication providers.
Create a new config file to hold the Ory Polis configuration values.
<?php
return [
'host' => 'http://localhost:5225', // Ory Polis service URL
'product' => 'your-app-name',
'client_id' => 'dummy', // Keep this as `dummy`, we'll pass the tenant & product as dynamic params
'client_secret' => 'dummy', // Keep this as `dummy`, we'll pass the tenant & product as dynamic params
'redirect' => env('APP_URL') . '/sso/callback'
];
Set host
to URL of running Ory Polis service.
Let's add a custom provider to the Laravel Socialite for the Ory Polis.
<?php
namespace App\BoxyHQ;
use Laravel\Socialite\Two\AbstractProvider;
use Laravel\Socialite\Two\ProviderInterface;
use Laravel\Socialite\Two\User;
use GuzzleHttp\RequestOptions;
class JacksonProvider extends AbstractProvider implements ProviderInterface
{
/**
* The Ory Polis instance host.
*
* @var string
*/
protected $host;
/**
* Set the Ory Polis instance host.
*
* @param string|null $host
* @return $this
*/
public function setHost($host)
{
if (! empty($host)) {
$this->host = rtrim($host, '/');
}
return $this;
}
protected function getAuthUrl($state)
{
return $this->buildAuthUrlFromBase($this->host . '/api/oauth/authorize', $state);
}
protected function getTokenUrl()
{
return $this->host . '/api/oauth/token';
}
protected function getUserByToken($token)
{
$response = $this->getHttpClient()->get($this->host . '/api/oauth/userinfo', [
RequestOptions::QUERY => ['access_token' => $token],
]);
return json_decode($response->getBody(), true);
}
protected function mapUserToObject(array $user)
{
return (new User)->setRaw($user)->map([
'id' => $user['id'],
'email' => $user['email'],
'name' => $user['firstName'].' '.$user['lastName'],
'first_name' => $user['firstName'],
'last_name' => $user['lastName'],
'requested' => $user['requested'],
'nickname' => null,
'avatar' => null,
]);
}
}
Bootstrap the JacksonProvider
in the AppServiceProvider
.
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\BoxyHQ\JacksonProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
$socialite = $this->app->make('Laravel\Socialite\Contracts\Factory');
$socialite->extend(
'jackson',
function ($app) use ($socialite) {
$config = config('jackson');
return $socialite->buildProvider(JacksonProvider::class, $config)
->setHost($config['host'] ?? null);
}
);
}
}
Make authentication request
Let's add a route to begin the authenticate flow; this route initiates the SAML SSO flow by redirecting the users to their configured Identity Provider.
<?php
use Illuminate\Support\Facades\Route;
Route::post('/sso', '\App\Http\Controllers\AuthController@store');
The store
method of AuthController
takes care of redirecting the user to the Identity Provider.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Laravel\Socialite\Facades\Socialite;
class AuthController extends Controller
{
public function store(Request $request)
{
$jackson = config('jackson');
$tenant = $request->input('tenant');
return Socialite::driver('jackson')
->with(['tenant' => $tenant, 'product' => $jackson['product']])
->redirect();
}
}
Fetch user profile
Let's add another route for receiving the callback after the authentication. Ensure the route matches the value of the redirect
you configured previously.
<?php
Route::get('/sso/callback', '\App\Http\Controllers\AuthController@callback');
The callback
method of AuthController
takes care of fetching the user profile if the authorization is valid.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Laravel\Socialite\Facades\Socialite;
class AuthController extends Controller
{
public function callback(Request $request)
{
$user = Socialite::driver('jackson')->user();
// $user has all the properties you need. Do your business logic here.
}
}
Authenticate user
Once the user has been retrieved from the Identity Provider, you may determine if the user exists in your application and authenticate the user. If the user does not exist in your application, you will typically create a new record in your database to represent the user.