🚗🏍️ Welcome to Motoshare!

Turning Idle Vehicles into Shared Rides & New Earnings.
Why let your bike or car sit idle when it can earn for you and move someone else forward?

From Idle to Income. From Parked to Purpose.
Earn by Sharing, Ride by Renting.
Where Owners Earn, Riders Move.
Owners Earn. Riders Move. Motoshare Connects.

With Motoshare, every parked vehicle finds a purpose. Partners earn. Renters ride. Everyone wins.

Start Your Journey with Motoshare

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.

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