This comprehensive guide will set up Single Sign-On (SSO) across all your Laravel applications using Keycloak as the Identity Provider. When a user logs into one application, they will automatically be authenticated in all other applications.
Architecture Overview
- Keycloak: Central Identity Provider (IdP)
- Laravel App 1: Your custom Laravel application
- Laravel App 2: Another Laravel application
- Eventmie Pro: Event management system
- SSO Flow: Login once → Access all applications seamlessly
Part 1: Keycloak Installation and Configuration
Step 1.1: Install Keycloak
Using Docker (recommended for development):
bash# Create a directory for Keycloak
mkdir keycloak-sso && cd keycloak-sso
# Run Keycloak with Docker
docker run -d \
--name keycloak-sso \
-p 8080:8080 \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=admin123 \
-v $(pwd)/keycloak-data:/opt/keycloak/data \
quay.io/keycloak/keycloak:latest start-dev
Step 1.2: Configure Keycloak Realm
- Access Keycloak Admin Console: http://localhost:8080
- Login: admin / admin123
- Create New Realm:
- Click “Create Realm”
- Name:
laravel-sso-realm
- Click “Create”
Step 1.3: Create Keycloak Client
- Navigate to Clients → Click “Create client”
- Client Configuration:
- Client type:
OpenID Connect
- Client ID:
laravel-multi-app-client
- Click “Next”
- Client type:
- Capability config:
- Client authentication:
ON
- Authorization:
OFF
- Standard flow:
ON
- Direct access grants:
ON
- Click “Next”
- Client authentication:
- Login settings:
- Valid redirect URIs: text
http://laravel-app1.local/auth/keycloak/callback http://laravel-app2.local/auth/keycloak/callback http://eventmie.local/auth/keycloak/callback http://eventmie.local/admin/auth/keycloak/callback
- Valid post logout redirect URIs: text
http://laravel-app1.local/ http://laravel-app2.local/ http://eventmie.local/
- Web origins:
*
- Click “Save”
- Valid redirect URIs: text
- Get Client Secret:
- Go to “Credentials” tab
- Copy the “Client secret” (you’ll need this later)
Step 1.4: Configure User Roles
- Create Realm Roles:
- Go to “Realm roles” → Click “Create role”
- Create these roles:
app_user
(basic user)app_admin
(admin across all apps)eventmie_organizer
(Eventmie organizer)eventmie_admin
(Eventmie admin)eventmie_customer
(Eventmie customer)
- Create Test Users:
- Go to “Users” → Click “Create new user”
- Username:
testuser
- Email:
test@example.com
- First name:
Test
- Last name:
User
- Click “Create”
- Go to “Credentials” tab → Set password → Turn off “Temporary”
- Go to “Role mapping” tab → Assign roles
Part 2: Prepare All Laravel Applications
Step 2.1: Install Required Packages (Do this for ALL Laravel apps)
For each Laravel application:
bash# Navigate to each Laravel project directory and run:
composer require laravel/socialite
composer require socialiteproviders/keycloak
composer require guzzlehttp/guzzle
Step 2.2: Environment Configuration (All Apps)
For Laravel App 1 (laravel-app1/.env
):
textAPP_NAME="Laravel App 1"
APP_URL=http://laravel-app1.local
# Keycloak Configuration
KEYCLOAK_BASE_URL=http://localhost:8080
KEYCLOAK_REALM=laravel-sso-realm
KEYCLOAK_CLIENT_ID=laravel-multi-app-client
KEYCLOAK_CLIENT_SECRET=your-client-secret-from-keycloak
KEYCLOAK_REDIRECT_URI="http://laravel-app1.local/auth/keycloak/callback"
# Session Configuration for SSO
SESSION_DRIVER=database
SESSION_DOMAIN=.local
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
For Laravel App 2 (laravel-app2/.env
):
textAPP_NAME="Laravel App 2"
APP_URL=http://laravel-app2.local
# Keycloak Configuration
KEYCLOAK_BASE_URL=http://localhost:8080
KEYCLOAK_REALM=laravel-sso-realm
KEYCLOAK_CLIENT_ID=laravel-multi-app-client
KEYCLOAK_CLIENT_SECRET=your-client-secret-from-keycloak
KEYCLOAK_REDIRECT_URI="http://laravel-app2.local/auth/keycloak/callback"
# Session Configuration for SSO
SESSION_DRIVER=database
SESSION_DOMAIN=.local
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
For Eventmie Pro (eventmie/.env
):
textAPP_NAME="Eventmie Pro"
APP_URL=http://eventmie.local
# Keycloak Configuration
KEYCLOAK_BASE_URL=http://localhost:8080
KEYCLOAK_REALM=laravel-sso-realm
KEYCLOAK_CLIENT_ID=laravel-multi-app-client
KEYCLOAK_CLIENT_SECRET=your-client-secret-from-keycloak
KEYCLOAK_REDIRECT_URI="http://eventmie.local/auth/keycloak/callback"
KEYCLOAK_ADMIN_REDIRECT_URI="http://eventmie.local/admin/auth/keycloak/callback"
# Session Configuration for SSO
SESSION_DRIVER=database
SESSION_DOMAIN=.local
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
Step 2.3: Services Configuration (All Apps)
Add to config/services.php
in ALL applications:
php<?php
// config/services.php
return [
// ... existing services
'keycloak' => [
'client_id' => env('KEYCLOAK_CLIENT_ID'),
'client_secret' => env('KEYCLOAK_CLIENT_SECRET'),
'redirect' => env('KEYCLOAK_REDIRECT_URI'),
'base_url' => env('KEYCLOAK_BASE_URL'),
'realm' => env('KEYCLOAK_REALM'),
],
];
Step 2.4: EventServiceProvider Configuration (All Apps)
Update app/Providers/EventServiceProvider.php
in ALL applications:
php<?php
// app/Providers/EventServiceProvider.php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
'SocialiteProviders\\Keycloak\\KeycloakExtendSocialite@handle',
],
];
public function boot()
{
//
}
}
Part 3: Database Setup (All Apps)
Step 3.1: User Migration (All Apps)
Create migration to add Keycloak fields:
bash# Run in each Laravel app directory
php artisan make:migration add_keycloak_fields_to_users_table
For Standard Laravel Apps (database/migrations/xxx_add_keycloak_fields_to_users_table.php
):
php<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddKeycloakFieldsToUsersTable extends Migration
{
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('keycloak_id')->nullable()->unique();
$table->json('keycloak_roles')->nullable();
$table->timestamp('last_keycloak_sync')->nullable();
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['keycloak_id', 'keycloak_roles', 'last_keycloak_sync']);
});
}
}
For Eventmie Pro (same migration but ensure compatibility with Eventmie’s user structure):
php<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddKeycloakFieldsToUsersTable extends Migration
{
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('keycloak_id')->nullable()->unique();
$table->json('keycloak_roles')->nullable();
$table->timestamp('last_keycloak_sync')->nullable();
// Ensure Eventmie-specific fields exist
if (!Schema::hasColumn('users', 'is_organiser')) {
$table->boolean('is_organiser')->default(false);
}
if (!Schema::hasColumn('users', 'is_admin')) {
$table->boolean('is_admin')->default(false);
}
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['keycloak_id', 'keycloak_roles', 'last_keycloak_sync']);
});
}
}
Step 3.2: Session Tables (All Apps)
bash# Run in each Laravel app directory
php artisan session:table
php artisan migrate
Part 4: SSO Service Class (Create in All Apps)
Create a shared SSO service in each app at app/Services/SSOService.php
:
php<?php
// app/Services/SSOService.php
namespace App\Services;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Log;
class SSOService
{
protected $client;
protected $keycloakBaseUrl;
protected $realm;
protected $clientId;
protected $clientSecret;
public function __construct()
{
$this->client = new Client();
$this->keycloakBaseUrl = env('KEYCLOAK_BASE_URL');
$this->realm = env('KEYCLOAK_REALM');
$this->clientId = env('KEYCLOAK_CLIENT_ID');
$this->clientSecret = env('KEYCLOAK_CLIENT_SECRET');
}
public function checkKeycloakSession()
{
try {
$response = $this->client->get($this->getAuthUrl(), [
'query' => [
'client_id' => $this->clientId,
'response_type' => 'code',
'scope' => 'openid',
'redirect_uri' => env('KEYCLOAK_REDIRECT_URI'),
'prompt' => 'none'
],
'allow_redirects' => false,
'cookies' => request()->cookies,
]);
return $response->getStatusCode() === 302 &&
strpos($response->getHeader('Location')[0] ?? '', 'code=') !== false;
} catch (\Exception $e) {
Log::error('SSO Session Check Error: ' . $e->getMessage());
return false;
}
}
public function refreshToken($refreshToken)
{
try {
$response = $this->client->post($this->getTokenUrl(), [
'form_params' => [
'grant_type' => 'refresh_token',
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
'refresh_token' => $refreshToken,
]
]);
return json_decode($response->getBody(), true);
} catch (\Exception $e) {
Log::error('Token Refresh Error: ' . $e->getMessage());
return false;
}
}
public function extractRolesFromToken($token)
{
try {
$tokenParts = explode('.', $token);
$payload = json_decode(base64_decode($tokenParts[1]), true);
return $payload['realm_access']['roles'] ?? [];
} catch (\Exception $e) {
Log::error('Token Role Extraction Error: ' . $e->getMessage());
return [];
}
}
private function getAuthUrl()
{
return $this->keycloakBaseUrl . '/realms/' . $this->realm . '/protocol/openid-connect/auth';
}
private function getTokenUrl()
{
return $this->keycloakBaseUrl . '/realms/' . $this->realm . '/protocol/openid-connect/token';
}
public function getLogoutUrl($postLogoutRedirectUri = null)
{
$logoutUrl = $this->keycloakBaseUrl . '/realms/' . $this->realm . '/protocol/openid-connect/logout';
if ($postLogoutRedirectUri) {
$logoutUrl .= '?post_logout_redirect_uri=' . urlencode($postLogoutRedirectUri);
}
return $logoutUrl;
}
}
Part 5: Keycloak Controllers (Create for Each App)
Step 5.1: Standard Laravel Apps Controller
Create app/Http/Controllers/Auth/KeycloakController.php
for regular Laravel apps:
php<?php
// app/Http/Controllers/Auth/KeycloakController.php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\SSOService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite;
use Illuminate\Http\Request;
class KeycloakController extends Controller
{
protected $ssoService;
public function __construct(SSOService $ssoService)
{
$this->ssoService = $ssoService;
}
public function redirect()
{
return Socialite::driver('keycloak')->redirect();
}
public function callback()
{
try {
$keycloakUser = Socialite::driver('keycloak')->user();
// Extract roles from token
$roles = $this->ssoService->extractRolesFromToken($keycloakUser->token);
// Create or update user
$user = User::updateOrCreate(
['email' => $keycloakUser->getEmail()],
[
'name' => $keycloakUser->getName(),
'email' => $keycloakUser->getEmail(),
'keycloak_id' => $keycloakUser->getId(),
'keycloak_roles' => json_encode($roles),
'password' => Hash::make(Str::random(24)),
'email_verified_at' => now(),
'last_keycloak_sync' => now(),
]
);
Auth::login($user);
// Store SSO session data
session([
'keycloak_token' => $keycloakUser->token,
'keycloak_refresh_token' => $keycloakUser->refreshToken,
'sso_authenticated' => true,
'user_roles' => $roles
]);
return redirect()->intended('/dashboard');
} catch (\Exception $e) {
\Log::error('Keycloak Auth Error: ' . $e->getMessage());
return redirect('/login')->with('error', 'Authentication failed: ' . $e->getMessage());
}
}
public function logout(Request $request)
{
$postLogoutUri = url('/');
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect($this->ssoService->getLogoutUrl($postLogoutUri));
}
}
Step 5.2: Eventmie Pro Controller
Create app/Http/Controllers/Auth/EventmieKeycloakController.php
for Eventmie:
php<?php
// app/Http/Controllers/Auth/EventmieKeycloakController.php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Services\SSOService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite;
use Illuminate\Http\Request;
// Use Eventmie's User model
use Classiebit\Eventmie\Models\User;
class EventmieKeycloakController extends Controller
{
protected $ssoService;
public function __construct(SSOService $ssoService)
{
$this->ssoService = $ssoService;
}
public function redirect()
{
return Socialite::driver('keycloak')->redirect();
}
public function adminRedirect()
{
return Socialite::driver('keycloak')
->with(['redirect_uri' => env('KEYCLOAK_ADMIN_REDIRECT_URI')])
->redirect();
}
public function callback()
{
try {
$keycloakUser = Socialite::driver('keycloak')->user();
$roles = $this->ssoService->extractRolesFromToken($keycloakUser->token);
// Create or update Eventmie user
$user = User::updateOrCreate(
['email' => $keycloakUser->getEmail()],
[
'first_name' => $this->getFirstName($keycloakUser->getName()),
'last_name' => $this->getLastName($keycloakUser->getName()),
'email' => $keycloakUser->getEmail(),
'keycloak_id' => $keycloakUser->getId(),
'keycloak_roles' => json_encode($roles),
'password' => Hash::make(Str::random(24)),
'email_verified_at' => now(),
'last_keycloak_sync' => now(),
'is_organiser' => in_array('eventmie_organizer', $roles) ? 1 : 0,
'is_admin' => in_array('eventmie_admin', $roles) || in_array('app_admin', $roles) ? 1 : 0,
]
);
Auth::login($user);
// Store SSO session data
session([
'keycloak_token' => $keycloakUser->token,
'keycloak_refresh_token' => $keycloakUser->refreshToken,
'sso_authenticated' => true,
'user_roles' => $roles
]);
return $this->redirectBasedOnRole($user);
} catch (\Exception $e) {
\Log::error('Eventmie Keycloak Auth Error: ' . $e->getMessage());
return redirect('/login')->with('error', 'Authentication failed');
}
}
public function adminCallback()
{
try {
$keycloakUser = Socialite::driver('keycloak')
->with(['redirect_uri' => env('KEYCLOAK_ADMIN_REDIRECT_URI')])
->user();
$roles = $this->ssoService->extractRolesFromToken($keycloakUser->token);
// Check admin privileges
if (!in_array('eventmie_admin', $roles) && !in_array('app_admin', $roles)) {
return redirect('/')->with('error', 'Insufficient admin privileges');
}
$user = User::updateOrCreate(
['email' => $keycloakUser->getEmail()],
[
'first_name' => $this->getFirstName($keycloakUser->getName()),
'last_name' => $this->getLastName($keycloakUser->getName()),
'email' => $keycloakUser->getEmail(),
'keycloak_id' => $keycloakUser->getId(),
'keycloak_roles' => json_encode($roles),
'password' => Hash::make(Str::random(24)),
'email_verified_at' => now(),
'is_admin' => 1,
'last_keycloak_sync' => now(),
]
);
Auth::login($user);
session([
'keycloak_token' => $keycloakUser->token,
'keycloak_refresh_token' => $keycloakUser->refreshToken,
'sso_authenticated' => true,
'user_roles' => $roles
]);
return redirect('/admin/dashboard');
} catch (\Exception $e) {
return redirect('/admin/login')->with('error', 'Admin authentication failed');
}
}
private function getFirstName($fullName)
{
$names = explode(' ', $fullName);
return $names[0] ?? '';
}
private function getLastName($fullName)
{
$names = explode(' ', $fullName);
return isset($names[1]) ? implode(' ', array_slice($names, 1)) : '';
}
private function redirectBasedOnRole($user)
{
if ($user->is_admin) {
return redirect('/admin/dashboard');
} elseif ($user->is_organiser) {
return redirect('/organiser/dashboard');
} else {
return redirect('/');
}
}
public function logout(Request $request)
{
$postLogoutUri = url('/');
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect($this->ssoService->getLogoutUrl($postLogoutUri));
}
}
Part 6: SSO Middleware (Create for All Apps)
Create app/Http/Middleware/SSOMiddleware.php
in ALL applications:
php<?php
// app/Http/Middleware/SSOMiddleware.php
namespace App\Http\Middleware;
use App\Services\SSOService;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class SSOMiddleware
{
protected $ssoService;
public function __construct(SSOService $ssoService)
{
$this->ssoService = $ssoService;
}
public function handle(Request $request, Closure $next)
{
// If user is already authenticated, continue
if (Auth::check()) {
// Check if token needs refresh
$this->refreshTokenIfNeeded();
return $next($request);
}
// Check for existing SSO session
if ($this->hasActiveSSO()) {
// Redirect to SSO login for silent authentication
return redirect()->route('keycloak.login');
}
return $next($request);
}
private function hasActiveSSO()
{
// Check if we have SSO session indicators
if (session('sso_authenticated')) {
return true;
}
// Check for active Keycloak session
return $this->ssoService->checkKeycloakSession();
}
private function refreshTokenIfNeeded()
{
$refreshToken = session('keycloak_refresh_token');
if ($refreshToken && $this->tokenNeedsRefresh()) {
$newTokens = $this->ssoService->refreshToken($refreshToken);
if ($newTokens) {
session([
'keycloak_token' => $newTokens['access_token'],
'keycloak_refresh_token' => $newTokens['refresh_token'] ?? $refreshToken
]);
}
}
}
private function tokenNeedsRefresh()
{
$token = session('keycloak_token');
if (!$token) return false;
try {
$tokenParts = explode('.', $token);
$payload = json_decode(base64_decode($tokenParts[1]), true);
$exp = $payload['exp'] ?? 0;
// Refresh if token expires in next 5 minutes
return ($exp - time()) < 300;
} catch (\Exception $e) {
return true; // Refresh on any token parsing error
}
}
}
Register middleware in app/Http/Kernel.php
for ALL apps:
phpprotected $routeMiddleware = [
// ... existing middleware
'sso' => \App\Http\Middleware\SSOMiddleware::class,
];
Part 7: Routes Configuration
Step 7.1: Standard Laravel Apps Routes
Add to routes/web.php
in regular Laravel apps:
php<?php
// routes/web.php
use App\Http\Controllers\Auth\KeycloakController;
// SSO Authentication Routes
Route::get('/auth/keycloak/redirect', [KeycloakController::class, 'redirect'])
->name('keycloak.login');
Route::get('/auth/keycloak/callback', [KeycloakController::class, 'callback'])
->name('keycloak.callback');
Route::post('/auth/keycloak/logout', [KeycloakController::class, 'logout'])
->name('keycloak.logout');
// Protected routes with SSO
Route::middleware(['sso'])->group(function () {
Route::get('/dashboard', function () {
return view('dashboard');
})->name('dashboard');
Route::get('/profile', function () {
return view('profile');
})->name('profile');
// Add your other protected routes here
});
// Home page (accessible to everyone)
Route::get('/', function () {
return view('welcome');
});
Step 7.2: Eventmie Pro Routes
Add to routes/web.php
in Eventmie:
php<?php
// routes/web.php
use App\Http\Controllers\Auth\EventmieKeycloakController;
// Eventmie SSO Routes
Route::get('/auth/keycloak/redirect', [EventmieKeycloakController::class, 'redirect'])
->name('keycloak.login');
Route::get('/auth/keycloak/callback', [EventmieKeycloakController::class, 'callback'])
->name('keycloak.callback');
// Admin SSO Routes
Route::get('/admin/auth/keycloak/redirect', [EventmieKeycloakController::class, 'adminRedirect'])
->name('admin.keycloak.login');
Route::get('/admin/auth/keycloak/callback', [EventmieKeycloakController::class, 'adminCallback'])
->name('admin.keycloak.callback');
// Logout
Route::post('/auth/keycloak/logout', [EventmieKeycloakController::class, 'logout'])
->name('keycloak.logout');
// Apply SSO middleware to Eventmie routes
Route::middleware(['sso'])->group(function () {
// User dashboard
Route::get('/dashboard', 'EventmieController@dashboard')->name('dashboard');
// Organiser routes
Route::prefix('organiser')->middleware(['auth', 'organiser'])->group(function () {
// Eventmie organiser routes
});
// Booking routes
Route::prefix('bookings')->middleware(['auth'])->group(function () {
// Eventmie booking routes
});
});
// Admin routes
Route::middleware(['sso', 'auth', 'admin'])->prefix('admin')->group(function () {
Route::get('/dashboard', 'AdminController@dashboard');
// Other admin routes
});
Part 8: Frontend Integration
Step 8.1: Standard Laravel Login View
Update resources/views/auth/login.blade.php
in regular Laravel apps:
text@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Login') }}</div>
<div class="card-body">
{{-- Regular Login Form --}}
<form method="POST" action="{{ route('login') }}">
@csrf
<div class="row mb-3">
<label for="email" class="col-md-4 col-form-label text-md-end">{{ __('Email Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email" autofocus>
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-3">
<label for="password" class="col-md-4 col-form-label text-md-end">{{ __('Password') }}</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="current-password">
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="row mb-0">
<div class="col-md-8 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Login') }}
</button>
</div>
</div>
</form>
<hr class="my-4">
{{-- SSO Login Button --}}
<div class="text-center">
<h5 class="mb-3">Or</h5>
<a href="{{ route('keycloak.login') }}" class="btn btn-success btn-lg btn-block">
<i class="fas fa-sign-in-alt"></i>
Sign in with SSO
</a>
<p class="text-muted mt-2">
<small>Login once, access all applications</small>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
Step 8.2: Eventmie Login View
Update Eventmie’s login view (usually in resources/views/eventmie/auth/login.blade.php
):
text@extends('eventmie::layouts.app')
@section('content')
<div class="lgx-page-wrapper">
<section>
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<div class="lgx-login-form-box">
<div class="lgx-login-form-title">
<h3>{{ __('eventmie-pro::em.login') }}</h3>
</div>
{{-- Regular Eventmie Login Form --}}
<form method="POST" action="{{ eventmie_url().'/login' }}">
@csrf
{{-- Your existing Eventmie form fields --}}
<div class="form-group">
<input type="email" name="email" class="form-control" placeholder="{{ __('eventmie-pro::em.email') }}" required>
</div>
<div class="form-group">
<input type="password" name="password" class="form-control" placeholder="{{ __('eventmie-pro::em.password') }}" required>
</div>
<button type="submit" class="btn btn-primary btn-block">
{{ __('eventmie-pro::em.login') }}
</button>
</form>
{{-- SSO Section --}}
<div class="sso-divider my-4">
<div class="text-center">
<span class="divider-text bg-white px-3 text-muted">OR</span>
</div>
</div>
<div class="sso-login-section">
<a href="{{ route('keycloak.login') }}" class="btn btn-success btn-lg btn-block mb-3">
<i class="fas fa-key"></i>
{{ __('Sign in with SSO') }}
</a>
@if(request()->is('admin*'))
<a href="{{ route('admin.keycloak.login') }}" class="btn btn-info btn-lg btn-block">
<i class="fas fa-shield-alt"></i>
{{ __('Admin SSO Login') }}
</a>
@endif
</div>
</div>
</div>
</div>
</div>
</section>
</div>
<style>
.sso-divider {
position: relative;
}
.sso-divider::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: #ddd;
}
.divider-text {
position: relative;
z-index: 1;
}
</style>
@endsection
Step 8.3: Navigation Bar Updates (All Apps)
Update navigation to include SSO logout:
text{{-- For regular Laravel apps --}}
<nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
<div class="container">
{{-- Navigation items --}}
<div class="navbar-nav ms-auto">
@guest
<a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a>
<a class="nav-link" href="{{ route('keycloak.login') }}">{{ __('SSO Login') }}</a>
@else
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
{{ Auth::user()->name }}
</a>
<ul class="dropdown-menu">
<li>
<form method="POST" action="{{ route('keycloak.logout') }}">
@csrf
<button type="submit" class="dropdown-item">
{{ __('Logout') }}
</button>
</form>
</li>
</ul>
</li>
@endguest
</div>
</div>
</nav>
Part 9: JavaScript for Seamless SSO
Step 9.1: SSO Detection Script
Create public/js/sso-detector.js
for ALL applications:
javascript// public/js/sso-detector.js
class SSODetector {
constructor() {
this.checkInterval = 30000; // Check every 30 seconds
this.keycloakBaseUrl = document.querySelector('meta[name="keycloak-base-url"]')?.content;
this.keycloakRealm = document.querySelector('meta[name="keycloak-realm"]')?.content;
this.init();
}
init() {
// Check SSO status on page load
this.checkSSOStatus();
// Set up periodic checks
this.startPeriodicCheck();
// Listen for storage events (cross-tab communication)
window.addEventListener('storage', (e) => {
if (e.key === 'sso_logout') {
this.handleCrossTabLogout();
}
});
}
async checkSSOStatus() {
try {
// Check if user is logged in locally
const isLoggedIn = document.querySelector('meta[name="user-authenticated"]')?.content === 'true';
if (!isLoggedIn) {
// Check if SSO session exists
const hasSSOSession = await this.checkKeycloakSession();
if (hasSSOSession) {
// Redirect to SSO login
window.location.href = '/auth/keycloak/redirect';
}
}
} catch (error) {
console.log('SSO check error:', error);
}
}
async checkKeycloakSession() {
if (!this.keycloakBaseUrl || !this.keycloakRealm) {
return false;
}
try {
// Create invisible iframe to check Keycloak session
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = `${this.keycloakBaseUrl}/realms/${this.keycloakRealm}/protocol/openid-connect/login-status-iframe.html`;
document.body.appendChild(iframe);
return new Promise((resolve) => {
iframe.onload = () => {
iframe.contentWindow.postMessage(
'checkSessionState',
`${this.keycloakBaseUrl}`
);
};
window.addEventListener('message', function handler(event) {
if (event.origin !== iframe.contentWindow.location.origin) {
return;
}
window.removeEventListener('message', handler);
document.body.removeChild(iframe);
resolve(event.data === 'authenticated');
});
// Timeout after 5 seconds
setTimeout(() => {
window.removeEventListener('message', () => {});
if (document.body.contains(iframe)) {
document.body.removeChild(iframe);
}
resolve(false);
}, 5000);
});
} catch (error) {
return false;
}
}
startPeriodicCheck() {
setInterval(() => {
this.checkSSOStatus();
}, this.checkInterval);
}
handleCrossTabLogout() {
// If logout happened in another tab, redirect to login
window.location.href = '/login';
}
// Call this when user logs out
notifyLogout() {
localStorage.setItem('sso_logout', Date.now());
localStorage.removeItem('sso_logout'); // Clean up immediately
}
}
// Initialize SSO detector when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new SSODetector();
});
// Global logout function
window.ssoLogout = function() {
// Notify other tabs
if (window.SSODetector) {
window.SSODetector.notifyLogout();
}
// Perform logout
const form = document.createElement('form');
form.method = 'POST';
form.action = '/auth/keycloak/logout';
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
if (csrfToken) {
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = '_token';
csrfInput.value = csrfToken;
form.appendChild(csrfInput);
}
document.body.appendChild(form);
form.submit();
};
Step 9.2: Include Script in Layouts
Add meta tags and script to your main layout (ALL apps):
text{{-- resources/views/layouts/app.blade.php --}}
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
{{-- SSO Meta Tags --}}
<meta name="keycloak-base-url" content="{{ env('KEYCLOAK_BASE_URL') }}">
<meta name="keycloak-realm" content="{{ env('KEYCLOAK_REALM') }}">
<meta name="user-authenticated" content="{{ auth()->check() ? 'true' : 'false' }}">
<title>{{ config('app.name', 'Laravel') }}</title>
{{-- Styles --}}
@vite(['resources/sass/app.scss', 'resources/js/app.js'])
</head>
<body>
<div id="app">
{{-- Your app content --}}
@yield('content')
</div>
{{-- SSO Detection Script --}}
<script src="{{ asset('js/sso-detector.js') }}"></script>
</body>
</html>
Part 10: Local Development Setup
Step 10.1: Configure Local Hosts
Add to your /etc/hosts
file (Windows: C:\Windows\System32\drivers\etc\hosts
):
text127.0.0.1 laravel-app1.local
127.0.0.1 laravel-app2.local
127.0.0.1 eventmie.local
Step 10.2: Virtual Host Configuration
For Apache (/etc/apache2/sites-available/
):
text# laravel-app1.local.conf
<VirtualHost *:80>
ServerName laravel-app1.local
DocumentRoot /path/to/laravel-app1/public
<Directory /path/to/laravel-app1/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
# laravel-app2.local.conf
<VirtualHost *:80>
ServerName laravel-app2.local
DocumentRoot /path/to/laravel-app2/public
<Directory /path/to/laravel-app2/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
# eventmie.local.conf
<VirtualHost *:80>
ServerName eventmie.local
DocumentRoot /path/to/eventmie/public
<Directory /path/to/eventmie/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
Enable sites:
bashsudo a2ensite laravel-app1.local.conf
sudo a2ensite laravel-app2.local.conf
sudo a2ensite eventmie.local.conf
sudo systemctl reload apache2
For Nginx:
text# /etc/nginx/sites-available/laravel-sso-apps
server {
listen 80;
server_name laravel-app1.local;
root /path/to/laravel-app1/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
server {
listen 80;
server_name laravel-app2.local;
root /path/to/laravel-app2/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
server {
listen 80;
server_name eventmie.local;
root /path/to/eventmie/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
Part 11: Testing the Complete SSO Setup
Step 11.1: Start All Services
bash# Start Keycloak
docker start keycloak-sso
# Verify Keycloak is running
curl http://localhost:8080/realms/laravel-sso-realm
# Start web server (Apache/Nginx)
sudo systemctl start apache2 # or nginx
Step 11.2: Test SSO Flow
- Initial Test:
- Visit
http://laravel-app1.local
- Click “Sign in with SSO”
- Login with Keycloak credentials
- Verify you’re redirected back to Laravel App 1
- Visit
- Cross-App SSO Test:
- While logged into App 1, visit
http://laravel-app2.local
- You should be automatically logged in (no login prompt)
- While logged into App 1, visit
- Eventmie Integration Test:
- Visit
http://eventmie.local
- Should automatically log you in
- Test organizer/admin access based on roles
- Visit
- Logout Test:
- Logout from any app
- Visit other apps – should require login again
Step 11.3: Role-Based Access Test
- Create different test users in Keycloak with different roles
- Test each role:
- Regular user: Should access basic features
- Organizer: Should access Eventmie organizer features
- Admin: Should access admin panels
Part 12: Production Deployment
Step 12.1: Environment Variables for Production
Update .env
files for production:
text# Use HTTPS URLs
KEYCLOAK_BASE_URL=https://keycloak.yourdomain.com
KEYCLOAK_REDIRECT_URI="https://app1.yourdomain.com/auth/keycloak/callback"
# Secure session configuration
SESSION_DOMAIN=.yourdomain.com
SESSION_SECURE=true
SESSION_SAME_SITE=none
# Enable Redis for session sharing
SESSION_DRIVER=redis
REDIS_HOST=your-redis-server
Step 12.2: SSL Certificate Setup
Ensure all applications have SSL certificates:
bash# Using Let's Encrypt with Certbot
sudo certbot --apache -d app1.yourdomain.com
sudo certbot --apache -d app2.yourdomain.com
sudo certbot --apache -d eventmie.yourdomain.com
sudo certbot --apache -d keycloak.yourdomain.com
Step 12.3: Keycloak Production Configuration
Update Keycloak redirect URIs for production:
texthttps://app1.yourdomain.com/auth/keycloak/callback
https://app2.yourdomain.com/auth/keycloak/callback
https://eventmie.yourdomain.com/auth/keycloak/callback
https://eventmie.yourdomain.com/admin/auth/keycloak/callback
Part 13: Troubleshooting Guide
Common Issues and Solutions
- “Invalid redirect URI” error:
- Check Keycloak client configuration
- Ensure all redirect URIs are added
- Verify correct protocol (http/https)
- Sessions not shared across apps:
- Verify
SESSION_DOMAIN
is set to root domain - Check that all apps use same session driver
- Ensure Redis/database is accessible by all apps
- Verify
- User not automatically logged in:
- Check SSO middleware is applied to routes
- Verify JavaScript SSO detector is loaded
- Check browser console for errors
- Role mapping issues:
- Verify roles are created in Keycloak
- Check token parsing in SSOService
- Ensure user model updates roles correctly
- Keycloak connection issues:
- Verify Keycloak is running and accessible
- Check network connectivity
- Verify client credentials
Debug Commands
bash# Check Keycloak realm configuration
curl http://localhost:8080/realms/laravel-sso-realm/.well-known/openid_configuration
# Test token validation
php artisan tinker
>>> app(\App\Services\SSOService::class)->extractRolesFromToken('your-token-here')
# Check session data
php artisan tinker
>>> session()->all()
# Clear Laravel caches
php artisan config:clear
php artisan cache:clear
php artisan route:clear
Conclusion
This complete setup provides seamless Single Sign-On across all your Laravel applications including Eventmie Pro. Users will:
- Login once in any application
- Automatically be authenticated when visiting other applications
- Maintain consistent user roles across all systems
- Logout from all applications with a single logout action
The setup ensures:
- ✅ Security: All authentication handled by Keycloak
- ✅ Scalability: Easy to add more Laravel applications
- ✅ User Experience: Seamless navigation between apps
- ✅ Role Management: Centralized user role assignment
- ✅ Session Management: Shared sessions across applications
Your users will now have a seamless experience moving between your Laravel applications and Eventmie Pro without needing to log in multiple times.

I’m Abhishek, a DevOps, SRE, DevSecOps, and Cloud expert with a passion for sharing knowledge and real-world experiences. I’ve had the opportunity to work with Cotocus and continue to contribute to multiple platforms where I share insights across different domains:
-
DevOps School – Tech blogs and tutorials
-
Holiday Landmark – Travel stories and guides
-
Stocks Mantra – Stock market strategies and tips
-
My Medic Plus – Health and fitness guidance
-
TrueReviewNow – Honest product reviews
-
Wizbrand – SEO and digital tools for businesses
I’m also exploring the fascinating world of Quantum Computing.