Introduction
Core Components of the Architecture
Authentication Flow (Login)
Middleware-Based Auto Login
Cross-Domain Session Control
Global Logout Architecture
Own Client Logout vs Peer Logout
Security Principles Behind the Design
Benefits of This Architecture
Introduction
Modern applications rarely live in isolation. Organizations often operate multiple web applications across different domains, all serving the same users. Requiring users to log in separately to each application leads to poor user experience, security risks, and operational complexity.
To solve this, we implement Single Sign-On (SSO) — a system where users authenticate once and gain access to multiple applications seamlessly.
This document explains a real-world, end-to-end SSO architecture built using:
Google OAuth for user-friendly authentication
Keycloak as the central identity provider (IdP)
Laravel applications acting as service providers
Middleware-based auto-login
Cross-domain token exchange
Global (broadcast) logout
The architecture focuses on security, scalability, and domain isolation, while still providing a smooth user experience.
Core Components of the Architecture
Combined Architecture + Full Flow Diagram (Google → Middleware → Logout)
Google OAuth (User Authentication Layer)
Google OAuth acts as the external authentication mechanism.
It answers one simple question:
“Is this user who they claim to be?”
Key responsibilities:
Provides a trusted login mechanism
Handles identity verification
Returns verified user attributes (email, name)
Google OAuth does not manage sessions across your applications — it only authenticates the user once.
- Keycloak (Central Identity Layer)
Keycloak sits at the heart of the architecture as the Identity Provider (IdP).
Its responsibilities include:
Managing realms, clients, and roles
Issuing access tokens
Validating tokens (introspection)
Acting as a central authority for identity trust
In this system:
Keycloak is not the UI login page
It operates behind the scenes
Laravel apps interact with it server-to-server
Keycloak becomes the single source of truth for identity and authorization.
- Laravel Applications (Service A, B, C…)
Each Laravel application represents an independent service:
Separate domain
Separate database
Separate user sessions
Separate cookies
Despite this separation, all services trust Keycloak-issued identity.
Roles of Laravel apps:
Maintain local user records
Store token references
Enforce authorization rules
Manage sessions
Authentication Flow (Login)
Step 1: User Logs In Using Google
The authentication journey starts when the user clicks “Login with Google” in the primary application (Service A).
Google authenticates the user
The application receives verified user information
A local Laravel session is created
At this stage:
The user is logged into one application only
Other applications are unaware of this login
public function handleGoogleCallback(Request $request)
{
Log::info('[GOOGLE_CB] handleGoogleCallback hit');
$inputrouteName = (string) session('login_from', 'login');
Log::info('[GOOGLE_CB] login_from', ['route' => $inputrouteName]);
$googleUser = Socialite::driver('google')->user();
$email = (string) ($googleUser->email ?? '');
$name = (string) ($googleUser->name ?? '');
if ($email === '') {
Log::error('[GOOGLE_CB] Missing email from Google');
return $this->redirectLoginError($inputrouteName, 'Email not received from Google');
}
$this->storeGoogleSession($request, $name, $email);
$user = $this->findUserByEmail($email);
if (!$user) {
return $this->redirectLoginError($inputrouteName, 'Email does not exist');
}
$this->loginUserAndRegenerate($request, $user);
// ✅ Reuse your common KC flow (same as OTP store)
$this->syncKeycloakAndStoreTokens($user);
return $this->redirectAfterLogin($user, $inputrouteName);
}
Step 2: Synchronization with Keycloak
After local login, the application synchronizes the user with Keycloak.
Key actions:
Ensures the user exists in Keycloak
Obtains Keycloak-issued tokens
Stores token metadata securely in the database
This step establishes federated identity trust:
Google proves who the user is
Keycloak becomes the authority for all services
public function syncKeycloakAndStoreTokens(User $user): array
{
// If resolveUser() is required, keep it; else remove to save latency
$this->kcSync->resolveUser($user->email);
$tokens = $this->syncWithKeycloak($user);
// Hard safety checks
if (!is_array($tokens) || empty($tokens['access_token']) || empty($tokens['expires_in'])) {
Log::error('[KC_AUTO] Invalid tokens returned from syncWithKeycloak()', [
'type' => is_array($tokens) ? 'array' : gettype($tokens),
'keys' => is_array($tokens) ? array_keys($tokens) : null,
]);
// Decide your policy:
// 1) Allow local login but skip KC token storage (recommended)
return [];
// 2) OR fail login:
// abort(response()->json(['success'=>false,'message'=>'Keycloak sync failed.'], 500));
}
$accessToken = (string) $tokens['access_token'];
$refreshToken = $tokens['refresh_token'] ?? null;
$expiresIn = (int) $tokens['expires_in'];
// Store only hash for access_token
$encAccess = encrypt($accessToken);
$encRefresh = $refreshToken ? encrypt($refreshToken) : null;
UserKeycloakToken::updateOrCreate(
['user_id' => $user->id],
[
'access_token' => $encAccess,
'refresh_token' => $encRefresh,
'expires_at' => time() + $expiresIn,
'revoked_at' => null,
]
);
Log::info('[KC_AUTO] Token stored', [
'user_id' => $user->id,
'expires_in' => $expiresIn,
'expires_at' => time() + $expiresIn,
'access_prefix' => substr($accessToken, 0, 16) . '...',
]);
return $tokens;
}
public function syncWithKeycloak(User $user, bool $storeInSession = true): array|false
{
$this->logStart($user);
$kcUserId = $this->getKcUserId($user);
if (!$kcUserId) {
$this->logFail('No kc_user_id stored', ['user_id' => $user->id, 'email' => $user->email]);
return false;
}
[$kcBase, $realm] = $this->getKcConfig();
if (!$kcBase || !$realm) {
$this->logFail('Missing KEYCLOAK_BASE_URL or KEYCLOAK_REALM');
return false;
}
$adminToken = $this->getAdminToken();
if (!$adminToken) {
$this->logFail('Admin token not available');
return false;
}
$dummyPassword = $this->makeDummyPassword();
// Step 1: remove required actions (best-effort)
$this->removeRequiredActions($kcBase, $realm, $kcUserId, $adminToken);
// Step 2: reset password (must succeed)
if (!$this->resetPassword($kcBase, $realm, $kcUserId, $adminToken, $dummyPassword)) {
return false;
}
// Step 3: login via password grant (must succeed)
$tokens = $this->loginWithPasswordGrant($kcBase, $realm, $user->email, $dummyPassword);
if ($tokens === false) {
return false;
}
// Optional: store in session
if ($storeInSession) {
$this->storeTokensInSession($tokens);
}
$this->logSuccess($user, $tokens);
return $tokens;
}
Step 3: Broadcasting Authentication to Other Domains
Once authenticated, the browser triggers a controlled broadcast to peer applications.
This broadcast:
Sends minimal identity context (email reference)
Does not expose sensitive credentials
Triggers identity setup in other domains
Each peer application:
Resolves the user locally
Establishes its own Keycloak trust
Sets its own secure authentication cookie
This preserves:
Domain isolation
Independent sessions
Shared identity trust
C:\myworkspace\motoshare-test\resources\views\dashboard.blade.php
<script>
window.MotoShareSSO = {
email: @json(auth()->check() ? auth()->user()->email : null),
accessToken: "{{ session('access_token') }}",
refreshToken: "{{ session('refresh_token') }}",
idToken: "{{ session('id_token') }}",
ssoDomains: @json(config('motoshare.sso_domains')),
currentOrigin: window.location.origin
};
</script>
Route::post('/auth/token-exchange', [\App\Http\Controllers\Api\PartnerController::class, 'exchange'])
->middleware(['web', 'corsfix']);
// Handle OPTIONS preflight
Route::options('/auth/token-exchange', function () {
return response()->json('OK', 204);
})->middleware(['web', 'corsfix']);
public function exchange(Request $request)
{
Log::info("Request Data exchange:", $request->all());
Log::info('[EXCHANGE] Request received', [
'host' => $request->getHost(),
'email' => $request->email,
]);
$host = $request->getHost();
$xsrfCookie = $request->cookie('XSRF-TOKEN');
log::info("mycsrf cookies");
log::info($xsrfCookie);
// Laravel session token (CSRF token stored in session)
$csrfSession = $request->session()->token();
\Log::info('[XSRF_DEBUG]', [
'host' => $host,
'xsrf_cookie_present' => !empty($xsrfCookie),
'xsrf_cookie_preview' => $xsrfCookie ? substr($xsrfCookie, 0, 20) . '...' : null,
'csrf_session_token_preview' => $csrfSession ? substr($csrfSession, 0, 20) . '...' : null,
'all_cookie_names' => array_keys($request->cookies->all()),
]);
// 1️⃣ Validate email
$email = trim((string) $request->email);
log::info($email);
$user = $this->kcSync->resolveUser($email);
$tokens = $this->ac->syncKeycloakAndStoreTokens($user, false);
session([
'kc_email' => $email,
]);
Log::info('[EXCHANGE] Email stored in session', [
'email' => session('kc_email')
]);
// 4️⃣ SAFE token logging (DO NOT LOG FULL TOKEN)
Log::info('[EXCHANGE] Tokens received', [
'has_access_token' => !empty($tokens['access_token']),
'access_token_len' => isset($tokens['access_token'])
? strlen($tokens['access_token'])
: 0,
'has_refresh_token' => !empty($tokens['refresh_token']),
'expires_in' => $tokens['expires_in'] ?? null,
'token_type' => $tokens['token_type'] ?? null,
]);
$accessToken = (string) $tokens['access_token'];
$refreshToken = (string) $tokens['refresh_token'];
if ($accessToken === '') {
return response()->json(['error' => 'No access token'], 400);
}
// 1) Validate token (access first, fallback refresh/offline)
// 3) Decide cookie domain
$cookieDomain = $this->resolveCookieDomain($request);
// 4) Optional: sync+store KC tokens for that user (reuse function)
$this->syncUserTokensIfExists($email);
// 5) Set cookie (access token only)
$cookieAccess = $this->makeAccessCookie($accessToken, $cookieDomain, $request);
// 6) Response with CORS headers
return $this->corsJsonSuccess($request, $cookieAccess);
}
Middleware-Based Auto Login
Why Middleware Is Critical
Users should not manually log in again when visiting another application.
This is handled by Laravel middleware, which runs before protected routes.
What the Middleware Does
On every request:
Reads the authentication cookie
Validates the token structure
Checks token validity (expiry, issuer, audience)
Extracts user identity
Confirms the token has not been revoked
Automatically logs the user into Laravel
The middleware acts as a security gatekeeper:
Prevents expired or forged tokens
Ensures revoked sessions cannot reappear
Maintains stateless verification logic
This design enables transparent SSO without sharing sessions across domains.
Cross-Domain Session Control
Token Storage as a Kill Switch
Each application stores token references in its own database.
This allows:
Centralized revocation
Session invalidation
Protection against replay attacks
Even if a browser still holds a cookie:
Middleware checks database state
Revoked tokens are immediately rejected
This ensures logout consistency and security.
class KeycloakAutoLogin
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
protected $utilityController;
protected $kcSync;
// Dependency Injection in constructor
public function __construct(
UtilityFunctionsController $utilityController,
KeycloakUserSyncService $kcSync,
KeycloakService $kc,
AuthenticatedSessionController $ac,
)
{
$this->utilityController = $utilityController;
$this->kc = $kc;
$this->ac = $ac;
$this->kcSync = $kcSync;
}
public function handle(Request $request, Closure $next)
{
Log::info('[KC_AUTO] check', [
'host' => $request->getHost(),
'url' => $request->fullUrl(),
'https'=> $request->secure() ? 'YES' : 'NO',
]);
// Optional: if already logged-in, skip auto login
$email = session('kc_email');
log::info("my session email");
log::info($email);
$host = $request->getHost();
$xsrfCookie = $request->cookie('XSRF-TOKEN');
log::info("mycsrf cookies");
log::info($xsrfCookie);
// Laravel session token (CSRF token stored in session)
$csrfSession = $request->session()->token();
\Log::info('[XSRF_DEBUG]', [
'host' => $host,
'xsrf_cookie_present' => !empty($xsrfCookie),
'xsrf_cookie_preview' => $xsrfCookie ? substr($xsrfCookie, 0, 20) . '...' : null,
'csrf_session_token_preview' => $csrfSession ? substr($csrfSession, 0, 20) . '...' : null,
'all_cookie_names' => array_keys($request->cookies->all()),
]);
$accessToken = $this->getAccessToken($request);
if (!$accessToken) {
return $next($request);
}
$payload = $this->decodeJwtPayload($accessToken);
if (!$payload) {
return $next($request);
}
if (!$this->passesJwtGuards($payload)) {
return $next($request);
}
$email = $this->extractEmail($payload);
Log::info('[KC_AUTO][DEBUG] email extraction result', [
'email' => $email,
]);
if (!$email) {
Log::warning('[KC_AUTO] JWT missing email/preferred_username');
return $next($request);
}
// Your existing logic: if local DB access_token is null then logout
if ($this->localTokenRowIsNull($email)) {
Log::warning('localTokenRowIsNulled_username');
$this->forceLogout($request);
return $next($request);
}
$claims = $this->extractClaims($payload);
return $this->autoLoginLaravelUser(
$claims['email'],
$claims['phone'],
$claims['kcUserId'],
$claims['username'],
$claims['role'],
$claims['status'],
$claims['address'],
$claims['sms_notification'],
$claims['email_notification'],
$claims['whatsapp_notification'],
$claims['email_verified_at'],
$claims['number_verified_at'],
$request,
$next
);
}
private function buildSafeReturnTo(Request $request): string
{
$path = $request->getPathInfo(); // "/drivers" or "/auth/keycloak/silent"
$qs = $request->getQueryString(); // "a=1&b=2"
// ✅ Never return_to silent routes (prevents recursion)
if (str_starts_with($path, '/auth/keycloak/silent')) {
return '/';
}
// ✅ Remove nested return_to if present in query
if ($qs) {
parse_str($qs, $arr);
if (isset($arr['return_to'])) {
unset($arr['return_to']);
$qs = http_build_query($arr);
}
}
$returnTo = $path . ($qs ? ('?' . $qs) : '');
// ✅ extra hard safety
if ($returnTo === '' || str_contains($returnTo, '/auth/keycloak/silent')) {
return '/';
}
return $returnTo;
}
/* ===================== Function Objects ===================== */
private function getAccessToken(Request $request): ?string
{
// 1) Best: Laravel parsed cookie
$token = $request->cookie('keycloak_access_token');
if (!empty($token)) {
return (string) $token;
}
// 2) Fallback: header parsing (your old logic, safer)
$cookieHeader = (string) $request->header('Cookie', '');
if ($cookieHeader === '') return null;
if (preg_match('/keycloak_access_token=([^;]+)/', $cookieHeader, $m)) {
return $m[1] ?? null;
}
return null;
}
private function passesJwtGuards(array $payload): bool
{
return $this->checkNotExpired($payload)
&& $this->checkIssuer($payload)
&& $this->checkAudience($payload);
}
private function checkNotExpired(array $payload): bool
{
$now = time();
$exp = $payload['exp'] ?? null;
if (!$exp || $exp < $now) {
Log::warning('[KC_AUTO] token expired', ['exp' => $exp, 'now' => $now]);
return false;
}
return true;
}
private function checkIssuer(array $payload): bool
{
$realm = (string) env('KEYCLOAK_REALM');
$base = rtrim((string) env('KEYCLOAK_BASE_URL'), '/');
$expected = "{$base}/realms/{$realm}";
$actual = $payload['iss'] ?? null;
if ($actual !== $expected) {
Log::warning('[KC_AUTO] iss mismatch', [
'expected' => $expected,
'actual' => $actual,
]);
return false;
}
return true;
}
private function checkAudience(array $payload): bool
{
$expectedAud = (string) env('KEYCLOAK_CLIENT_ID');
$aud = $payload['aud'] ?? [];
if (is_string($aud)) $aud = [$aud];
if (!is_array($aud)) $aud = [];
if ($expectedAud !== '' && !in_array($expectedAud, $aud, true)) {
Log::warning('[KC_AUTO] aud mismatch', [
'expected' => $expectedAud,
'actual' => $aud,
]);
return false;
}
return true;
}
private function extractEmail(array $payload): ?string
{
return $payload['email']
?? $payload['preferred_username']
?? null;
}
private function localTokenRowIsNull(string $email): bool
{
$user = User::query()->where('email', $email)->first();
log::info($user);
if (!$user) {
// If no user, do not logout, just skip auto-login
return false;
}
$row = UserKeycloakToken::where('user_id', $user->id)->value('access_token');
log::info($row);
return $row === null;
}
private function forceLogout(Request $request): void
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
Log::info('[KC_AUTO] forced logout (token row null)');
}
private function extractClaims(array $payload): array
{
$now = now(); // single Carbon instance
return [
'email' => $this->extractEmail($payload),
'phone' => $payload['phone'] ?? ($payload['phone_number'] ?? null),
'username' => $payload['preferred_username'] ?? null,
'kcUserId' => $payload['sub'] ?? null,
'role' => $payload['role'] ?? 'user',
'status' => $payload['status'] ?? 'active',
'address' => $payload['address'] ?? null,
// handle possible typo with space
'sms_notification' => $payload['sms_notification'] ?? ($payload[' sms_notification'] ?? "1"),
'email_notification' => $payload['email_notification'] ?? "1",
'whatsapp_notification' => $payload['whatsapp_notification'] ?? "1",
'email_verified_at' => $now,
'number_verified_at' => $now,
];
}
private function decodeJwtPayload(string $jwt): ?array
{
$parts = explode('.', $jwt);
if (count($parts) !== 3) {
Log::warning("⚠️ [KC_AUTO] JWT malformed, expected 3 parts", [
'parts_count' => count($parts),
]);
return null;
}
// [0] = header, [1] = payload, [2] = signature
$payloadB64 = $parts[1];
$json = $this->base64UrlDecode($payloadB64);
if ($json === null) {
Log::warning("⚠️ [KC_AUTO] Failed base64url decode for JWT payload");
return null;
}
$data = json_decode($json, true);
if (!is_array($data)) {
Log::warning("⚠️ [KC_AUTO] JWT payload is not valid JSON", [
'json' => $json,
]);
return null;
}
return $data;
}
/**
* Helper for base64url decoding (used by JWT).
*/
private function base64UrlDecode(string $input): ?string
{
$remainder = strlen($input) % 4;
if ($remainder) {
$padLen = 4 - $remainder;
$input .= str_repeat('=', $padLen);
}
$decoded = base64_decode(strtr($input, '-_', '+/'), true);
return $decoded === false ? null : $decoded;
}
private function autoLoginLaravelUser(
$email,
$phone,
$kcUserId,
$username,
$role,
$status,
$address,
$smsNotification,
$emailNotification,
$whatsappNotification,
$emailverified,
$numberverified,
Request $request,
Closure $next
) {
$email = (string) $email;
$user = $this->findOrCreateUserFromKc($email, $phone, $kcUserId, $username, $role, $status, $address,
$smsNotification, $emailNotification, $whatsappNotification,$emailverified,$numberverified
);
// ✅ reuse your function (only once)
$this->ac->syncKeycloakAndStoreTokens($user);
$this->loginAndRegenerate($request, $user);
return $next($request);
}
private function findOrCreateUserFromKc(
string $email,
$phone,
$kcUserId,
$username,
$role,
$status,
$address,
$smsNotification,
$emailNotification,
$whatsappNotification,
$emailverified,
$numberverified
): User {
$user = User::query()->where('email', $email)->first();
if (!$user) {
return $this->createUserFromKc($email, $phone, $kcUserId, $username, $role, $status, $address,
$smsNotification, $emailNotification, $whatsappNotification,$emailverified,$numberverified
);
}
$this->updateUserFromKc($user, $phone, $kcUserId, $username, $role, $status, $address,
$smsNotification, $emailNotification, $whatsappNotification,$emailverified,$numberverified
);
return $user;
}
private function createUserFromKc(
string $email,
$phone,
$kcUserId,
$username,
$role,
$status,
$address,
$smsNotification,
$emailNotification,
$whatsappNotification,
$emailverified,
$numberverified
): User {
$user = new User();
$user->name = $username ?: strstr($email, '@', true);
$user->email = $email;
// Your column is `number` (not phone)
$user->number = $phone ?: null;
$user->kc_user_id = $kcUserId ?: null;
$user->role = $role ?: 'user';
$user->status = $status ?: 'active';
$user->sms_notification = $smsNotification ?? "1";
$user->email_notification = $emailNotification ?? "1";
$user->whatsapp_notification = $whatsappNotification ?? "1";
$user->address = $address ?: null;
$user->email_verified_at = $emailverified ?? "1";
$user->number_verified_at = $numberverified ?: null;
$user->password = bcrypt(\Illuminate\Support\Str::random(32));
$user->save();
Log::info('[KC_AUTO] user created', [
'user_id' => $user->id,
'email' => $email,
]);
return $user;
}
private function updateUserFromKc(
User $user,
$phone,
$kcUserId,
$username,
$role,
$status,
$address,
$smsNotification,
$emailNotification,
$whatsappNotification,
$emailverified,
$numberverified
): void {
$updates = [];
// NOTE: DB column is number
if ($phone && $user->number !== $phone) {
$updates['number'] = $phone;
}
if ($kcUserId && $user->kc_user_id !== $kcUserId) {
$updates['kc_user_id'] = $kcUserId;
}
if ($username && $user->name !== $username) {
$updates['name'] = $username;
}
if ($role && $user->role !== $role) {
$updates['role'] = $role;
}
if ($status && $user->status !== $status) {
$updates['status'] = $status;
}
if ($address && $user->address !== $address) {
$updates['address'] = $address;
}
if ($smsNotification !== null && $user->sms_notification !== $smsNotification) {
$updates['sms_notification'] = $smsNotification;
}
if ($emailNotification !== null && $user->email_notification !== $emailNotification) {
$updates['email_notification'] = $emailNotification;
}
if ($whatsappNotification !== null && $user->whatsapp_notification !== $whatsappNotification) {
$updates['whatsapp_notification'] = $whatsappNotification;
}
if ($emailverified && $user->email_verified_at !== $emailverified) {
$updates['email_verified_at'] = $emailverified;
}
if ($numberverified && $user->number_verified_at !== $numberverified) {
$updates['number_verified_at'] = $numberverified;
}
if (!empty($updates)) {
$user->update($updates);
Log::info('[KC_AUTO] user updated', [
'user_id' => $user->id,
'updates' => array_keys($updates),
]);
}
}
private function loginAndRegenerate(Request $request, User $user): void
{
Auth::login($user);
$request->session()->regenerate();
}
}
Global Logout Architecture
Why Logout Is Hard in SSO
Logout is more complex than login because:
Multiple applications are involved
Sessions exist across domains
Browsers enforce cookie isolation
A simple local logout is insufficient.
Broadcast Logout Strategy
When a user logs out:
A browser-side broadcast notifies all peer applications
Each application:
Revokes its local token
Destroys its session
The originating application performs its own logout
This ensures:
No application remains authenticated
No stale session survives
User trust is maintained
public function destroy(Request $request)
{
$id = Auth()->user()->id;
UserKeycloakToken::where('user_id', $id)
->update([
'access_token' => null, // ✅ make it NULL
'revoked_at' => time(), // ✅ INT timestamp
]);
//====================old part below starts=========================//
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return response()->view('auth.logout-call-traccar');
// return redirect('/'); // ✅ THIS is missing in your code
}
public function ssologout(Request $request)
{
Log::info('[SSO_LOGOUT] Hit', [
'host' => $request->getHost(),
'origin' => $request->headers->get('origin'),
]);
// 1) Get email from request (you can send it from Domain A)
$email = $request->input('email');
// OPTIONAL fallback: try from Authorization token (if you send Bearer)
Log::info('[SSO_LOGOUT] Email received', ['email' => $email]);
// 2) Check if user exists in this domain DB
$user = User::where('email', $email)->first();
if (!$user) {
Log::warning('[SSO_LOGOUT] User not found in this domain, bypassing', [
'email' => $email,
'host' => $request->getHost(),
]);
return response()->json(['ok' => true, 'skipped' => 'user_not_found']);
}
// 3) Revoke tokens for this user in THIS domain
$affected = UserKeycloakToken::where('user_id', $user->id)
->update([
'access_token' => null, // ✅ make it NULL
'revoked_at' => time(), // ✅ INT timestamp
]);
Log::info('[SSO_LOGOUT] Tokens revoked in this domain', [
'user_id' => $user->id,
'affected' => $affected,
'host' => $request->getHost(),
]);
// OPTIONAL: also logout local session if any
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return response()->json([
'ok' => true,
'revoked' => $affected,
'domain' => $request->getHost(),
]);
}
Own Client Logout vs Peer Logout
The architecture distinguishes between:
Own client logout
The application where the logout originated
Peer logout
Other applications that must also revoke access
Both paths:
Clear token records
Destroy sessions
Prevent re-authentication via middleware
Security Principles Behind the Design
This architecture follows key security principles:
Zero Trust between services
No shared sessions
Token-based authentication
Database-backed revocation
Minimal data exchange
Defense-in-depth via middleware
Each application can independently enforce security without relying on others.
Benefits of This Architecture
Seamless user experience
Strong security boundaries
Scales across many domains
No cookie sharing hacks
Clear audit and revocation control
Easy to extend with roles and permissions
Conclusion
This end-to-end SSO architecture demonstrates how modern applications can implement secure, scalable, and user-friendly authentication across multiple domains.
By combining:
Google OAuth for user trust
Keycloak for identity authority
Laravel middleware for enforcement
Token-based session control
Broadcast logout for consistency
Top comments (0)