singleloginsystem/app/Http/Controllers/EntraController.php
= feaa8b6c93 Feat: Add modular URL app creation with role-based access and Microsoft integration support
- Introduced `StoreURLAppForm` and `StoreURLAppData` for form handling and data transformation.
- Added `UrlAppService` to streamline URL app creation with settings like `accessUrl` and `isConnectedToMicrosoft`.
- Enhanced user access controls with role-based permissions using `UserAccessTypeEnum`.
- Updated UI for URL app creation with support for logos and dynamic role selection.
- Implemented feature tests to validate URL app creation and access URL formatting.
2026-06-22 07:21:26 +00:00

217 lines
7.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\{OauthToken, User};
use App\Services\UserAppAccessService;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Http\{RedirectResponse, Request};
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
class EntraController extends Controller
{
// ── OAuth config from .env ────────────────────────────────────────────────
private const string PROVIDER = 'microsoft';
private const string SCOPES = 'openid profile email offline_access User.Read Mail.Read Mail.ReadWrite Mail.Send';
private string $tenantId;
private string $clientId;
private string $clientSecret;
private string $redirectUri;
public function __construct()
{
$this->tenantId = config('services.microsoft.tenant_id');
$this->clientId = config('services.microsoft.client_id');
$this->clientSecret = config('services.microsoft.client_secret');
$this->redirectUri = route('entra.callback');
}
// ── Step 1: Redirect user to Microsoft login ──────────────────────────────
public function connect(#[CurrentUser] User $user): RedirectResponse
{
$user = auth()->user();
if (! $user) {
abort(403, 'Unauthorized');
}
$state = Str::random(40);
session(['oauth_state' => $state]);
$query = http_build_query([
'client_id' => $this->clientId,
'response_type' => 'code',
'redirect_uri' => $this->redirectUri,
'response_mode' => 'query',
'scope' => self::SCOPES,
'state' => $state,
'prompt' => 'select_account',
]);
return redirect("https://login.microsoftonline.com/$this->tenantId/oauth2/v2.0/authorize?$query");
}
// ── Step 2: Handle callback, exchange code for tokens ────────────────────
public function callback(Request $request): RedirectResponse
{
$user = auth()->user();
if (! $user) {
abort(403, 'Unauthorized');
}
$activeServices = app(UserAppAccessService::class)->getActiveServices($user);
$hasAzureFdAccess = $activeServices->contains(fn ($service) => 'azure-fd' === $service->providerSlug);
if (! $hasAzureFdAccess) {
abort(403, 'You do not have permission to access the required application.');
}
// CSRF state check
if ($request->state !== session('oauth_state')) {
return redirect()->route('dashboard.user')->with('entra_error', 'Invalid OAuth state. Please try again.');
}
if ($request->has('error')) {
return redirect()->route('dashboard.user')->with('entra_error', $request->error_description ?? 'Microsoft login failed.');
}
// Exchange authorization code for tokens
$response = Http::asForm()->post(
"https://login.microsoftonline.com/$this->tenantId/oauth2/v2.0/token",
[
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
'code' => $request->code,
'redirect_uri' => $this->redirectUri,
'grant_type' => 'authorization_code',
]
);
if ($response->failed()) {
return redirect()->route('dashboard.user')->with('entra_error', 'Failed to retrieve tokens from Microsoft.');
}
$data = $response->json();
// Persist token for this user (upsert so re-connecting just updates)
OauthToken::updateOrCreate(
[
'user_id' => auth()->id(),
'provider' => self::PROVIDER,
],
[
'access_token' => $data['access_token'],
'refresh_token' => $data['refresh_token'] ?? null,
'token_type' => $data['token_type'] ?? 'Bearer',
'scopes' => self::SCOPES,
'expires_at' => now()->addSeconds($data['expires_in'] ?? 3600),
]
);
return redirect()->route('dashboard.user')->with('entra_success', 'Microsoft connected successfully!');
}
// ── Open Entra (refresh token silently if expired) ───────────────────────
public function openEntra(): RedirectResponse
{
$user = auth()->user();
if (! $user) {
abort(403, 'Unauthorized');
}
$activeServices = app(UserAppAccessService::class)->getActiveServices($user);
$hasAzureFdAccess = $activeServices->contains(fn ($service) => 'azure-fd' === $service->providerSlug);
if (! $hasAzureFdAccess) {
abort(403, 'You do not have permission to access the required application.');
}
$token = $user->oauthToken(self::PROVIDER);
if (! $token) {
return redirect()->route('entra.connect');
}
// If token is expired, try to refresh silently
if ($token->isExpired()) {
$refreshed = $this->refreshToken($token);
if (! $refreshed) {
// Refresh failed — force re-login
$token->delete();
return redirect()->route('entra.connect');
}
}
// Redirect to Outlook Web — SSO session handles silent sign-in
return redirect('https://outlook.office.com/mail/');
}
// ── Disconnect ────────────────────────────────────────────────────────────
private function refreshToken(OauthToken $token): bool
{
if (! $token->refresh_token) {
return false;
}
$response = Http::asForm()->post(
"https://login.microsoftonline.com/$this->tenantId/oauth2/v2.0/token",
[
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
'refresh_token' => $token->refresh_token,
'grant_type' => 'refresh_token',
'scope' => self::SCOPES,
]
);
if ($response->failed()) {
return false;
}
$data = $response->json();
$token->update([
'access_token' => $data['access_token'],
'refresh_token' => $data['refresh_token'] ?? $token->refresh_token,
'expires_at' => now()->addSeconds($data['expires_in'] ?? 3600),
]);
return true;
}
// ── Internal: Refresh access token using refresh_token ───────────────────
public function disconnect(): RedirectResponse
{
$user = auth()->user();
if (! $user) {
abort(403, 'Unauthorized');
}
$activeServices = app(UserAppAccessService::class)->getActiveServices($user);
$hasAzureFdAccess = $activeServices->contains(fn ($service) => 'azure-fd' === $service->providerSlug);
if (! $hasAzureFdAccess) {
abort(403, 'You do not have permission to access the required application.');
}
$user->oauthToken(self::PROVIDER)?->delete();
return redirect()->route('dashboard.user')->with('entra_success', 'Microsoft disconnected.');
}
}