Implementing Single Sign-On (SSO) with Laravel , Step by Step
Laravel provides solutions for SSO (Single Sign-On) authentication with Laravel Passport , enabling users to access multiple applications with a single set of credentials, making it easy to implement a robust and secure SSO solution.
I will share my solution here and the code at the end .
Introduction
We will create 2 Laravel projects and we’ll use Laravel Passport for OAuth2 authorization .
The Auth project — will be responsible to allows and centralize those users .
The Supervisor project — it will be the backend for the adminstrator area for supervisor users .
Step 1 — Installing and Setting Laravel AuthProject
At this step, we will create the first application . the Auth Aplication .
# Create a new Laravel Project
- composer create-project laravel/laravel .
Install Laravel Breze
- composer require laravel/breeze --dev php artisan breeze:install
- php artisan breeze:install
Set Laravel Passport Clients -
## CREATE THE PASSPORT CLIENT
- php artisan passport:client
Which user ID should the client be assigned to?:
> 1
What should we name the client?:
> Auth
## CREATE PASSPORT -- PASSWORD
php artisan passport:client --password --name=UserAdmin --provider=users
The database , will be like this -
Set the .env file with those passport users -
DB_CONNECTION=mysql
DB_HOST=172.23.0.1
DB_PORT=3306
DB_DATABASE=laravel_database_sso
DB_USERNAME=root
DB_PASSWORD=secret
PASSPORT_LOGIN_ENDPOINT="http://localhost:8081/oauth/token"
PASSPORT_CLIENT_ID="9c93bc86-7f11-4c1c-83ee-9ceb590b4e6f"
PASSPORT_CLIENT_SECRET="GLaFWG6NUFnx9f8w4JIeT1eXK7Y54LESrKlCsPbe"
PASSPORT_PASSWORD_ID="9be3009b-66e2-4400-97d2-92112d7db4cb"
PASSPORT_PASSWORD_SECRET="ksOW88P9jhPPD4sfBBezEYLS2B3FS7iv5DGdxyrJ"
Create Users Tables , Roles and Permissions Table
class CreateUserAdmins extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::defaultStringLength(191);
Schema::create('user_admins', function (Blueprint $table) {
$table->engine = 'InnoDB';
$table->bigIncrements('id');
$table->string('email')->unique();
$table->string('password');
$table->rememberToken();
$table->enum('status', ['active', 'no_active' ])->default('active');
$table->string('folder' , '60')->nullable();
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('user_admins');
}
}
class CreateUserAdminInfos extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::defaultStringLength(191);
Schema::create('user_admin_infos', function (Blueprint $table) {
$table->engine = 'InnoDB';
$table->bigIncrements('id');
$table->bigInteger('user_id')->unsigned();
$table->foreign('user_id')->references('id')->on('user_admins')->onDelete('cascade');
$table->string('cpf');
// $table->string('cpf')->unique();
$table->string('name');
$table->string('last_name')->nullable();
$table->string('phone')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('user_admin_infos', function(Blueprint $table){
$table->dropForeign('user_admin_infos_user_id_foreign');
});
Schema::dropIfExists('user_admin_infos');
}
}
class CreateUserUserAdminRoles extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::defaultStringLength(191);
Schema::create('user_user_admin_roles', function (Blueprint $table) {
$table->engine = 'InnoDB';
$table->bigInteger('user_id')->unsigned()->index();
$table->foreign('user_id')->references('id')->on('user_admins')->onDelete('cascade');
$table->integer('role_id')->unsigned()->index();
$table->foreign('role_id')->references('id')->on('user_admin_roles')->onDelete('cascade');
// $table->unique(['user_id','role_id']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('user_user_admin_roles', function(Blueprint $table){
$table->dropForeign('user_user_admin_roles_user_id_foreign');
$table->dropForeign('user_user_admin_roles_role_id_foreign');
});
Schema::dropIfExists('user_user_admin_roles');
}
}
class createUserAdminAccessAreasTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::defaultStringLength(191);
Schema::create('user_admin_access_areas', function (Blueprint $table) {
$table->engine = 'InnoDB';
$table->increments('id');
$table->string('title', 40)->unique();
$table->string('url_title', 40)->unique();
$table->text('description');
$table->text('image')->nullable();
$table->string('url', 300);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('user_admin_access_areas');
}
};
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::defaultStringLength(191);
Schema::create('user_admin_user_admin_access_areas', function (Blueprint $table) {
$table->engine = 'InnoDB';
$table->bigInteger('user_id')->unsigned()->index();
$table->foreign('user_id')->references('id')->on('user_admins')->onDelete('cascade');
$table->integer('area_id')->unsigned()->index();
$table->foreign('area_id')->references('id')->on('user_admin_access_areas')->onDelete('cascade');
// $table->unique(['user_id','area_id']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('user_admin_user_admin_access_areas', function(Blueprint $table){
$table->dropForeign('user_admin_user_admin_access_areas_user_id_foreign');
$table->dropForeign('user_admin_user_admin_access_areas_area_id_foreign');
});
Schema::dropIfExists('user_admin_user_admin_access_areas');
}
};
Create Models
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;
class UserAdmin extends Authenticatable
{
use HasFactory, Notifiable , HasApiTokens;
protected $table = 'user_admins';
protected $fillable = [
'status',
'email',
'folder'
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
public function Roles(){
return $this->belongsToMany(UserAdminRole::class , 'user_user_admin_roles' , 'user_id' , 'role_id' );
}
public function hasRole($role)
{
if (is_string($role)) {
return $this->Roles->contains('title', $role);
}
}
public function AcessAreas(){
return $this->belongsToMany(UserAdminAccessArea::class , 'user_admin_user_admin_access_areas' , 'user_id' , 'area_id' );
}
public function hasAcessArea($role)
{
if (is_string($role)) {
return $this->Roles->contains('title', $role);
}
}
Migrate and Seed Files
## MIGRATE DB
- php artisan migrate
- php artisan db:seed
I changed the style -
Create the Admin Area
Now we can list all those areas that users has right to access -
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\UserAdmin;
use App\Models\UserAdminAccessArea;
use App\Traits\ApiResponse;
use Illuminate\Support\Facades\Auth;
/**
* HOME CONTROLLER
*/
class HomeController extends Controller
{
use ApiResponse;
public function __construct()
{
parent::__construct();
// verify admin user
$this->middleware('admin');
}
/**
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$user_id = Auth::user()->id;
$access_area = UserAdminAccessArea::select('title', 'description', 'url')
->whereHas('Users', function ($query) use ($user_id) {
$query->where('id', $user_id);
})->get();
$user = UserAdmin::select([ 'id', 'status', 'email'])
->with(['AdminInfo' => function ($query) {
$query->select('name', 'cpf', 'phone', 'last_name', 'user_id');
}])->where('id', $user_id)->first();
return view('admin/home', ['access_areas' => $access_area, 'user' => ['name' => $user->AdminInfo->name . " ". $user->AdminInfo->last_name, 'email' => $user->email] ]);
}
@extends('layouts.admin')
@section('content')
<div class="container">
<div class="columns estatistic-list">
<div class="column box-area" >
<div class="columns">
<div class="column has-text-centered header-pg">
<h1 class="main-text">Wecome</h1>
<h1 class="main-text">You have access to Areas</h1>
</div>
</div>
<div class="columns">
<div class="column has-text-centered">
<ul class="list-areas">
@foreach($access_areas as $list)
<li><a href="{{ $list['url'] }}" target="_blank" >
<h3>{{ $list['title'] }}</h3>
</a></li>
@endforeach
</ul>
</div>
</div>
</div>
</div>
</div>
@endsection
And now you can login in the area with the user and password -
Step 2— Create the Supervisor Service ( Second Laravel Project )
Now we going to create the second project — The Supervisor .
# Create a new Laravel Project
- composer create-project laravel/laravel .
Install Passport
php artisan passport:client --personal
What should we name the personal access client? :
> Supervisor
Next step we will , add the information of the Auth Service connection in .env -
DB_CONNECTION=mysql
DB_HOST=172.23.0.1
DB_PORT=3306
DB_DATABASE=laravel_database_supervisor
DB_USERNAME=root
DB_PASSWORD=secret
AUTH_HTTP_HOST="http://localhost:8081"
AUTH_REQUEST_HOST="http://laravel_auth_sso_server:80"
AUTH_CLIENT_ID="9c93bc86–7f11–4c1c-83ee-9ceb590b4e6f"
AUTH_CLIENT_SECRET="GLaFWG6NUFnx9f8w4JIeT1eXK7Y54LESrKlCsPbe"
AUTH_SCOPE="access-supervisor-area"
At those guards and providers at confi/auth.php
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
'hash' => false,
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| If you have multiple user tables or models you may configure multiple
| sources which represent each model / table. These sources may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\UserAdmin::class,
],
Create as well an User table , model and database for the Supervisor -
class CreateUserAdmins extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('user_admins', function (Blueprint $table) {
$table->engine = 'InnoDB';
$table->bigIncrements('id');
$table->string('email')->unique();
$table->rememberToken();
$table->enum('status', ['active', 'no_active'])->default('active');
$table->string('folder', '60')->nullable();
$table->string('auth_token', '400')->nullable();
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('user_admins');
}
We have now two services, with 2 Laravel projects -
We now have now 2 databases -
Step 4 — Requesting Tokens
Now what we need to do is to get from the Supervisor Service -
- The authorize link ( from Auth Service ).
- The token ( from Auth Service ).
- And get the user information ( with the token that we got before ).
we can separate it in two methods at the supervisor service -
1. Get The Auth Link
In this method, it will access the route — {auth_service}/oauth/authorize .
With the authorization code .
public function getAuthLink()
{
$state = Str::random(40);
$query = http_build_query([
'client_id' => $this->client_id,
'redirect_url' => $this->callback_url,
'response_type' => 'code',
'scope' => $this->scope,
'state' => $state,
]);
return response()->json(['authorize_url' => config('services.auth.http_host').'/oauth/authorize?'.$query, 'state' => $state], 200);
}
The result will be like this on postman -
It will return a authorize_url and state , with this link we will be able to access the url via browser —
{
"authorize_url": "http://localhost:8081/oauth/authorize?client_id=9c93bc86-7f11-4c1c-83ee-9ceb590b4e6f&redirect_url=http%3A%2F%2Flocalhost%3A8001%2Fcallback&response_type=code&scope=access-supervisor-area&state=klwOGbu1z7EvE2MPRCYamPu3lPuODDdtmEIi7s8v",
"state": "klwOGbu1z7EvE2MPRCYamPu3lPuODDdtmEIi7s8v"
}
Accessing with your e-mail and password , it will return back to supervisor service , on the route {supervisor_service}/api/auth/callback -
this call back url , is set previously on auth service database , on table oauth_clients .
now you will be redirected to -
We logged in Auth service and now we were redirected back to supervisor service at the callback method .
Route::get('/auth/callback', [AuthController::class, 'getCallBack']);
2. Get Call Back
At the call back method, we will -
Get the Token , Get the User detail and Register new User in Supervisor Service .
we can get the token accessing the URL — {auth_service}/oauth/token .
$sso_token = $this->getAuthToken($params['state'], $params['code']);
private function getAuthToken($state, $code)
{
throw_unless(strlen($state) > 0 && $state, InvalidArgumentException::class);
$response = Http::asForm()->post(
$this->request_host.'/oauth/token',
[
'grant_type' => 'authorization_code',
'client_id' => $this->client_id,
'client_secret' => $this->client_secret,
'redirect_url' => $this->callback_url,
'code' => $code,
]);
return $response->json()['access_token'];
}
Now that we have the token ( the auth laravel project token ), we can access
and get the user data that we need to register in supervisor service .
private function getUserDetaleAndRegisterNewUser($access_token, $area = 'supervisor')
{
$response = Http::withHeaders([
'Accept' => 'application/json',
])->withToken($access_token)->get($this->request_host.'/api/user?area='.$area);
$userData = $response->json();
$userAdmin = UserAdmin::updateOrCreate(
["email" => $userData['email']],
[
'status' => 'active',
'email' => $userData['email']
]);
$action = $userAdmin->AdminInfo()->exists() ? 'update' : 'create';
$userAdmin->AdminInfo()->{$action}([
'name' => $userData['name'],
'last_name' => $userData['last_name'],
'cpf' => $userData['cpf'],
'phone' => $userData['phone'],
'manager_status' => $userData['manager_status'],
]);
return $userAdmin;
}
$user = $this->getUserDetaleAndRegisterNewUser($sso_token);
if (!$user) {
return response()->json(['status' => 'not-authorized'], 400);
}
// return the token for access this laravel project
return $user->createToken('Supervisor')->accessToken;
Now on database , it will create a new register with the new Supervisor User -
And finally it will return a token -
I just printed that on screen but in a real project you could send a request for a api .
Now it will be able to access the supervisor area giving a new permission for the user .
Now in this protected route -
we will be able to access with the token -
it returns the user information accessing the supervisor service -
{
"email": "annamarie18@example.net",
"name": "Luigi",
"last_name": "Purdy",
"is_manager": false
}
we can see that the user now has permission accessing the supervisor protected routes links with the token .
Conclusion
Laravel Passport makes it straightforward to implement SSO authentication using OAuth2. By leveraging the authorize
method and the built-in OAuth2 capabilities of Passport, developers can create a seamless authentication experience across multiple applications, improving security and user convenience.
Keep exploring Laravel’s ecosystem to unlock even more possibilities for your web development projects. Happy coding!
Git Hub Code -
Thanks a lot for reading till end. Follow or contact me via:
Github:https://github.com/murilolivorato
LinkedIn: https://www.linkedin.com/in/murilo-livorato-80985a4a/
Youtube : https://www.youtube.com/@murilolivorato1489