diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index fe7c424..f984eb6 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -48,12 +48,13 @@ public function launchSSO($id) ? 'organizations' : 'consumers'; - // Redirect to Microsoft OAuth authorize page with login_hint, domain_hint and custom callback + // Redirect to Microsoft OAuth authorize page with login_hint, domain_hint, prompt=none and custom callback return Socialite::driver('microsoft') ->redirectUrl(route('sso.callback')) ->with([ 'login_hint' => $user->email, - 'domain_hint' => $domainHint + 'domain_hint' => $domainHint, + 'prompt' => 'none' ]) ->redirect(); } catch (\Exception $e) { @@ -74,6 +75,14 @@ public function launchSSO($id) */ public function handleSSOCallback(Request $request) { + // If Microsoft returns an error (user logged out or session expired) + if ($request->has('error')) { + Auth::logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + return redirect()->route('login')->with('error', 'You have been signed out because your Microsoft session is no longer active.'); + } + $user = Auth::user(); try { @@ -87,12 +96,20 @@ public function handleSSOCallback(Request $request) 'microsoft_token' => $microsoftUser->token, 'microsoft_refresh_token' => $microsoftUser->refreshToken ?? $user->microsoft_refresh_token, ]); + + // Update verification timestamp + session(['ms_session_last_checked' => now()]); } } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error('Microsoft SSO callback failed', [ 'message' => $e->getMessage() ]); - // If callback fails but we have target URL, we can still try to redirect + + // Treat token exchange failure as an expired session + Auth::logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + return redirect()->route('login')->with('error', 'Your Microsoft session has expired. Please log in again.'); } $targetUrl = session('sso_target_url', 'https://outlook.office.com/mail/'); @@ -110,6 +127,53 @@ public function handleSSOCallback(Request $request) return redirect($targetUrl); } + /** + * Handle silent session verification callback from Microsoft. + */ + public function handleSessionCheckCallback(Request $request) + { + // If Microsoft returns an error (user logged out or session expired) + if ($request->has('error')) { + Auth::logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + return redirect()->route('login')->with('error', 'You have been signed out because you signed out of your Microsoft account.'); + } + + try { + $microsoftUser = Socialite::driver('microsoft') + ->redirectUrl(route('sso.callback-check')) + ->user(); + + $user = Auth::user(); + if ($microsoftUser && $user) { + // Update active tokens in DB + $user->update([ + 'microsoft_token' => $microsoftUser->token, + 'microsoft_refresh_token' => $microsoftUser->refreshToken ?? $user->microsoft_refresh_token, + ]); + + // Update verification timestamp + session(['ms_session_last_checked' => now()]); + } + } catch (\Exception $e) { + \Illuminate\Support\Facades\Log::error('Microsoft session check callback failed', [ + 'message' => $e->getMessage() + ]); + + // Treat token exchange failure as an expired session + Auth::logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + return redirect()->route('login')->with('error', 'Your Microsoft session has expired. Please log in again.'); + } + + // Redirect back to the page they were trying to access + $origin = session('sso_check_origin', route('dashboard')); + session()->forget('sso_check_origin'); + return redirect($origin); + } + /** * Render the desktop application launcher page with web fallback. */ diff --git a/app/Http/Middleware/VerifyMicrosoftSession.php b/app/Http/Middleware/VerifyMicrosoftSession.php new file mode 100644 index 0000000..a512c22 --- /dev/null +++ b/app/Http/Middleware/VerifyMicrosoftSession.php @@ -0,0 +1,57 @@ +microsoft_id) { + $lastChecked = session('ms_session_last_checked'); + + // Check every 30 seconds upon page access/refresh + if (!$lastChecked || now()->diffInSeconds($lastChecked) > 30) { + // Set timestamp immediately to prevent redirect loop + session(['ms_session_last_checked' => now()]); + + // Store target URL to return to after silent verification + session(['sso_check_origin' => $request->fullUrl()]); + + // Determine domain hint based on user's email domain + $domainHint = (str_ends_with($user->email, '.onmicrosoft.com') || (str_contains($user->email, '@') && !str_ends_with($user->email, 'outlook.com') && !str_ends_with($user->email, 'hotmail.com') && !str_ends_with($user->email, 'live.com'))) + ? 'organizations' + : 'consumers'; + + try { + // Redirect to Microsoft with prompt=none to check session status + return Socialite::driver('microsoft') + ->redirectUrl(route('sso.callback-check')) + ->with([ + 'login_hint' => $user->email, + 'domain_hint' => $domainHint, + 'prompt' => 'none' + ]) + ->redirect(); + } catch (\Exception $e) { + \Illuminate\Support\Facades\Log::error('Microsoft session silent verification failed', [ + 'message' => $e->getMessage() + ]); + } + } + } + + return $next($request); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index c00d09b..9c3efb3 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -14,6 +14,7 @@ ->withMiddleware(function (Middleware $middleware): void { $middleware->alias([ 'role' => \App\Http\Middleware\EnsureRole::class, + 'verify-ms-session' => \App\Http\Middleware\VerifyMicrosoftSession::class, ]); }) ->withExceptions(function (Exceptions $exceptions): void { diff --git a/public/Screenshot_5.png b/public/Screenshot_5.png new file mode 100644 index 0000000..f69f3d9 Binary files /dev/null and b/public/Screenshot_5.png differ diff --git a/public/Screenshot_6.png b/public/Screenshot_6.png new file mode 100644 index 0000000..cc976bc Binary files /dev/null and b/public/Screenshot_6.png differ diff --git a/routes/web.php b/routes/web.php index cf9facd..97f0491 100644 --- a/routes/web.php +++ b/routes/web.php @@ -19,16 +19,23 @@ Route::get('/auth/microsoft/logout', [LoginController::class, 'frontChannelLogout'])->name('auth.microsoft.logout'); // User Dashboard Portal (requires standard user role or admin access) -Route::middleware(['auth', 'role:user'])->group(function () { +Route::middleware(['auth', 'role:user', 'verify-ms-session'])->group(function () { Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard'); +}); + +Route::middleware(['auth', 'role:user'])->group(function () { Route::get('/sso/launch/{id}', [DashboardController::class, 'launchSSO'])->name('sso.launch'); Route::get('/sso/callback', [DashboardController::class, 'handleSSOCallback'])->name('sso.callback'); Route::get('/sso/launch-app', [DashboardController::class, 'launchApp'])->name('sso.launch-app'); }); +Route::middleware('auth')->group(function () { + Route::get('/sso/check-session-callback', [DashboardController::class, 'handleSessionCheckCallback'])->name('sso.callback-check'); +}); + // Admin Control Panel (requires admin role) Route::middleware(['auth', 'role:admin'])->group(function () { - Route::get('/admin/dashboard', [AdminController::class, 'index'])->name('admin.dashboard'); + Route::get('/admin/dashboard', [AdminController::class, 'index'])->name('admin.dashboard')->middleware('verify-ms-session'); Route::post('/admin/apps', [AdminController::class, 'storeApp'])->name('admin.apps.store'); Route::delete('/admin/apps/{id}', [AdminController::class, 'destroyApp'])->name('admin.apps.destroy'); Route::post('/admin/users/{id}/toggle-block', [AdminController::class, 'toggleBlockUser'])->name('admin.users.toggle-block');