implement microsoft autologout feature
This commit is contained in:
parent
a2e3ad20e7
commit
de3c74cc42
@ -36,15 +36,9 @@ public function launchSSO($id)
|
|||||||
return redirect()->route('dashboard')->with('error', 'Access denied. You do not have permission to access this service.');
|
return redirect()->route('dashboard')->with('error', 'Access denied. You do not have permission to access this service.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it is a Microsoft service
|
// If user is logged in via Microsoft, route all service launches (including Keka)
|
||||||
$isMicrosoft = str_contains($app->url, 'microsoft.com') ||
|
// through the Microsoft SSO flow to ensure their browser has an active Microsoft session.
|
||||||
str_contains($app->url, 'office.com') ||
|
if ($user->microsoft_id) {
|
||||||
str_contains($app->url, 'office365.com') ||
|
|
||||||
str_contains($app->url, 'outlook.com') ||
|
|
||||||
$app->icon_svg === 'outlook' ||
|
|
||||||
$app->icon_svg === 'teams';
|
|
||||||
|
|
||||||
if ($isMicrosoft && $user->microsoft_id) {
|
|
||||||
// Save target app URL in session so callback knows where to redirect
|
// Save target app URL in session so callback knows where to redirect
|
||||||
session(['sso_target_url' => $app->url]);
|
session(['sso_target_url' => $app->url]);
|
||||||
|
|
||||||
|
|||||||
@ -114,6 +114,9 @@ public function handleMicrosoftCallback(Request $request)
|
|||||||
if ($user->is_blocked) {
|
if ($user->is_blocked) {
|
||||||
return redirect()->route('login')->with('error', 'Your account has been blocked. Please contact the administrator.');
|
return redirect()->route('login')->with('error', 'Your account has been blocked. Please contact the administrator.');
|
||||||
}
|
}
|
||||||
|
if ($user->role === 'admin') {
|
||||||
|
return redirect()->route('admin.login')->with('error', 'Security protocol: Administrators are restricted to email and password authentication only.');
|
||||||
|
}
|
||||||
// Update existing user with Microsoft data if not already set
|
// Update existing user with Microsoft data if not already set
|
||||||
$user->update([
|
$user->update([
|
||||||
'microsoft_id' => $microsoftUser->getId(),
|
'microsoft_id' => $microsoftUser->getId(),
|
||||||
@ -155,8 +158,27 @@ public function logout(Request $request)
|
|||||||
$request->session()->invalidate();
|
$request->session()->invalidate();
|
||||||
$request->session()->regenerateToken();
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
// Redirect to Microsoft's logout URL to log the user out of Microsoft as well
|
||||||
|
try {
|
||||||
|
$microsoftLogoutUrl = Socialite::driver('microsoft')->getLogoutUrl(route('login'));
|
||||||
|
return redirect($microsoftLogoutUrl);
|
||||||
|
} catch (\Exception $e) {
|
||||||
return redirect()->route('login')->with('success', 'Logged out successfully.');
|
return redirect()->route('login')->with('success', 'Logged out successfully.');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle front-channel logout from Microsoft.
|
||||||
|
*/
|
||||||
|
public function frontChannelLogout(Request $request)
|
||||||
|
{
|
||||||
|
Auth::logout();
|
||||||
|
$request->session()->invalidate();
|
||||||
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
// Microsoft renders this in an iframe. A simple 200 response is expected.
|
||||||
|
return response('Logged out from SingleLogin', 200);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to redirect user depending on their role.
|
* Helper method to redirect user depending on their role.
|
||||||
|
|||||||
BIN
public/Screenshot_3.png
Normal file
BIN
public/Screenshot_3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
BIN
public/Screenshot_4.png
Normal file
BIN
public/Screenshot_4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
@ -529,7 +529,15 @@
|
|||||||
<div class="profile-email">
|
<div class="profile-email">
|
||||||
<span>{{ $user->email }}</span>
|
<span>{{ $user->email }}</span>
|
||||||
•
|
•
|
||||||
<span class="badge badge-user">{{ $user->role }}</span>
|
<span class="badge badge-user">
|
||||||
|
@if($user->isAdmin())
|
||||||
|
Admin
|
||||||
|
@elseif($user->roles->isNotEmpty())
|
||||||
|
{{ $user->roles->pluck('name')->implode(', ') }}
|
||||||
|
@else
|
||||||
|
{{ ucfirst($user->role) }}
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
@if($user->microsoft_id)
|
@if($user->microsoft_id)
|
||||||
<span class="badge badge-sso">Microsoft Linked</span>
|
<span class="badge badge-sso">Microsoft Linked</span>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
// Microsoft OAuth endpoints
|
// Microsoft OAuth endpoints
|
||||||
Route::get('/auth/microsoft/redirect', [LoginController::class, 'redirectToMicrosoft'])->name('auth.microsoft.redirect');
|
Route::get('/auth/microsoft/redirect', [LoginController::class, 'redirectToMicrosoft'])->name('auth.microsoft.redirect');
|
||||||
Route::get('/auth/microsoft/callback', [LoginController::class, 'handleMicrosoftCallback'])->name('auth.microsoft.callback');
|
Route::get('/auth/microsoft/callback', [LoginController::class, 'handleMicrosoftCallback'])->name('auth.microsoft.callback');
|
||||||
|
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'])->group(function () {
|
||||||
|
|||||||
@ -188,4 +188,92 @@ public function test_admin_cannot_authenticate_via_microsoft_oauth_callback(): v
|
|||||||
$response->assertSessionHas('error', 'Security protocol: Administrators are restricted to email and password authentication only.');
|
$response->assertSessionHas('error', 'Security protocol: Administrators are restricted to email and password authentication only.');
|
||||||
$this->assertFalse(auth()->check());
|
$this->assertFalse(auth()->check());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test local logout redirects to Microsoft global logout endpoint.
|
||||||
|
*/
|
||||||
|
public function test_user_logout_redirects_to_microsoft_logout_page(): void
|
||||||
|
{
|
||||||
|
$user = User::create([
|
||||||
|
'name' => 'Standard User',
|
||||||
|
'email' => 'user@sentientgeeks.com',
|
||||||
|
'role' => 'user',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mockSocialite = \Mockery::mock('Laravel\Socialite\Contracts\Provider');
|
||||||
|
$mockSocialite->shouldReceive('getLogoutUrl')
|
||||||
|
->with(route('login'))
|
||||||
|
->andReturn('https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri=' . urlencode(route('login')));
|
||||||
|
|
||||||
|
\Laravel\Socialite\Facades\Socialite::shouldReceive('driver')->with('microsoft')->andReturn($mockSocialite);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->get('/logout'); // supports GET for testing
|
||||||
|
|
||||||
|
$response->assertRedirect('https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri=' . urlencode(route('login')));
|
||||||
|
$this->assertFalse(auth()->check());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Microsoft Front-channel logout callback terminates local session.
|
||||||
|
*/
|
||||||
|
public function test_microsoft_front_channel_logout_clears_session(): void
|
||||||
|
{
|
||||||
|
$user = User::create([
|
||||||
|
'name' => 'Standard User',
|
||||||
|
'email' => 'user@sentientgeeks.com',
|
||||||
|
'role' => 'user',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
$this->assertTrue(auth()->check());
|
||||||
|
|
||||||
|
$response = $this->get('/auth/microsoft/logout');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertSee('Logged out from SingleLogin');
|
||||||
|
$this->assertFalse(auth()->check());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that launching any service (like Keka) routes through Microsoft SSO flow
|
||||||
|
* if the user is Microsoft-linked.
|
||||||
|
*/
|
||||||
|
public function test_sso_launch_redirects_to_microsoft_for_all_services_if_user_is_microsoft_linked(): void
|
||||||
|
{
|
||||||
|
$user = User::create([
|
||||||
|
'name' => 'Standard User',
|
||||||
|
'email' => 'user@sentientgeeks.com',
|
||||||
|
'role' => 'user',
|
||||||
|
'microsoft_id' => 'user-microsoft-id-123',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$app = App::create([
|
||||||
|
'name' => 'Keka HR',
|
||||||
|
'url' => 'https://sentientgeeks.keka.com',
|
||||||
|
'color' => '#f27059',
|
||||||
|
'tag' => 'HR Portal'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assign role with the app to user
|
||||||
|
$role = Role::create(['name' => 'Employee']);
|
||||||
|
$role->apps()->sync([$app->id]);
|
||||||
|
$user->roles()->sync([$role->id]);
|
||||||
|
|
||||||
|
$mockSocialite = \Mockery::mock('Laravel\Socialite\Contracts\Provider');
|
||||||
|
$mockSocialite->shouldReceive('redirectUrl')->with(route('sso.callback'))->andReturnSelf();
|
||||||
|
$mockSocialite->shouldReceive('with')->with([
|
||||||
|
'login_hint' => $user->email,
|
||||||
|
'domain_hint' => 'organizations'
|
||||||
|
])->andReturnSelf();
|
||||||
|
$mockSocialite->shouldReceive('redirect')
|
||||||
|
->andReturn(redirect('https://login.microsoftonline.com/common/oauth2/v2.0/authorize'));
|
||||||
|
|
||||||
|
\Laravel\Socialite\Facades\Socialite::shouldReceive('driver')->with('microsoft')->andReturn($mockSocialite);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->get(route('sso.launch', $app->id));
|
||||||
|
|
||||||
|
$response->assertRedirect('https://login.microsoftonline.com/common/oauth2/v2.0/authorize');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user