Compare commits

..

2 Commits

6 changed files with 134 additions and 5 deletions

View File

@ -48,12 +48,13 @@ public function launchSSO($id)
? 'organizations' ? 'organizations'
: 'consumers'; : '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') return Socialite::driver('microsoft')
->redirectUrl(route('sso.callback')) ->redirectUrl(route('sso.callback'))
->with([ ->with([
'login_hint' => $user->email, 'login_hint' => $user->email,
'domain_hint' => $domainHint 'domain_hint' => $domainHint,
'prompt' => 'none'
]) ])
->redirect(); ->redirect();
} catch (\Exception $e) { } catch (\Exception $e) {
@ -74,6 +75,14 @@ public function launchSSO($id)
*/ */
public function handleSSOCallback(Request $request) 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(); $user = Auth::user();
try { try {
@ -87,12 +96,20 @@ public function handleSSOCallback(Request $request)
'microsoft_token' => $microsoftUser->token, 'microsoft_token' => $microsoftUser->token,
'microsoft_refresh_token' => $microsoftUser->refreshToken ?? $user->microsoft_refresh_token, 'microsoft_refresh_token' => $microsoftUser->refreshToken ?? $user->microsoft_refresh_token,
]); ]);
// Update verification timestamp
session(['ms_session_last_checked' => now()]);
} }
} catch (\Exception $e) { } catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Microsoft SSO callback failed', [ \Illuminate\Support\Facades\Log::error('Microsoft SSO callback failed', [
'message' => $e->getMessage() '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/'); $targetUrl = session('sso_target_url', 'https://outlook.office.com/mail/');
@ -110,6 +127,53 @@ public function handleSSOCallback(Request $request)
return redirect($targetUrl); 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. * Render the desktop application launcher page with web fallback.
*/ */

View File

@ -0,0 +1,57 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Facades\Socialite;
use Symfony\Component\HttpFoundation\Response;
class VerifyMicrosoftSession
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
$user = Auth::user();
// Only check if user is authenticated and has a Microsoft ID linked
if ($user && $user->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);
}
}

View File

@ -14,6 +14,7 @@
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware): void {
$middleware->alias([ $middleware->alias([
'role' => \App\Http\Middleware\EnsureRole::class, 'role' => \App\Http\Middleware\EnsureRole::class,
'verify-ms-session' => \App\Http\Middleware\VerifyMicrosoftSession::class,
]); ]);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {

BIN
public/Screenshot_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
public/Screenshot_6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View File

@ -19,16 +19,23 @@
Route::get('/auth/microsoft/logout', [LoginController::class, 'frontChannelLogout'])->name('auth.microsoft.logout'); Route::get('/auth/microsoft/logout', [LoginController::class, 'frontChannelLogout'])->name('auth.microsoft.logout');
// User Dashboard Portal (requires standard user role or admin access) // 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::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/launch/{id}', [DashboardController::class, 'launchSSO'])->name('sso.launch');
Route::get('/sso/callback', [DashboardController::class, 'handleSSOCallback'])->name('sso.callback'); Route::get('/sso/callback', [DashboardController::class, 'handleSSOCallback'])->name('sso.callback');
Route::get('/sso/launch-app', [DashboardController::class, 'launchApp'])->name('sso.launch-app'); 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) // Admin Control Panel (requires admin role)
Route::middleware(['auth', 'role:admin'])->group(function () { 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::post('/admin/apps', [AdminController::class, 'storeApp'])->name('admin.apps.store');
Route::delete('/admin/apps/{id}', [AdminController::class, 'destroyApp'])->name('admin.apps.destroy'); 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'); Route::post('/admin/users/{id}/toggle-block', [AdminController::class, 'toggleBlockUser'])->name('admin.users.toggle-block');