- 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.
217 lines
7.4 KiB
PHP
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.');
|
|
}
|
|
}
|