Best Cosmetic Hospitals Near You

Compare top cosmetic hospitals, aesthetic clinics & beauty treatments by city.

Trusted • Verified • Best-in-Class Care

Explore Best Hospitals

Complete SSO Setup Guide: Keycloak as IdP for Multiple Laravel Applications (Including Eventmie Pro)

Uncategorized

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

  1. Access Keycloak Admin Console: http://localhost:8080
  2. Login: admin / admin123
  3. Create New Realm:
    • Click “Create Realm”
    • Name: laravel-sso-realm
    • Click “Create”

Step 1.3: Create Keycloak Client

  1. Navigate to Clients → Click “Create client”
  2. Client Configuration:
    • Client type: OpenID Connect
    • Client ID: laravel-multi-app-client
    • Click “Next”
  3. Capability config:
    • Client authentication: ON
    • Authorization: OFF
    • Standard flow: ON
    • Direct access grants: ON
    • Click “Next”
  4. Login settings:
    • Valid redirect URIs: texthttp://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: texthttp://laravel-app1.local/ http://laravel-app2.local/ http://eventmie.local/
    • Web origins: *
    • Click “Save”
  5. Get Client Secret:
    • Go to “Credentials” tab
    • Copy the “Client secret” (you’ll need this later)

Step 1.4: Configure User Roles

  1. 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)
  2. 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

  1. 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
  2. Cross-App SSO Test:
    • While logged into App 1, visit http://laravel-app2.local
    • You should be automatically logged in (no login prompt)
  3. Eventmie Integration Test:
    • Visit http://eventmie.local
    • Should automatically log you in
    • Test organizer/admin access based on roles
  4. Logout Test:
    • Logout from any app
    • Visit other apps – should require login again

Step 11.3: Role-Based Access Test

  1. Create different test users in Keycloak with different roles
  2. 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

  1. “Invalid redirect URI” error:
    • Check Keycloak client configuration
    • Ensure all redirect URIs are added
    • Verify correct protocol (http/https)
  2. 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
  3. User not automatically logged in:
    • Check SSO middleware is applied to routes
    • Verify JavaScript SSO detector is loaded
    • Check browser console for errors
  4. Role mapping issues:
    • Verify roles are created in Keycloak
    • Check token parsing in SSOService
    • Ensure user model updates roles correctly
  5. 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:

  1. Login once in any application
  2. Automatically be authenticated when visiting other applications
  3. Maintain consistent user roles across all systems
  4. 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.

Best Cardiac Hospitals Near You

Discover top heart hospitals, cardiology centers & cardiac care services by city.

Advanced Heart Care • Trusted Hospitals • Expert Teams

View Best Hospitals
0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x