149 lines
5.1 KiB
PHP
149 lines
5.1 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\Tenant;
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Str;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
class RbacDelegatedAuthController extends Controller
|
|
{
|
|
public function start(Request $request): RedirectResponse
|
|
{
|
|
$tenantIdentifier = $request->string('tenant')->toString();
|
|
$tenant = $tenantIdentifier
|
|
? Tenant::query()->forTenant($tenantIdentifier)->firstOrFail()
|
|
: Tenant::current();
|
|
|
|
$targetTenant = $tenantIdentifier ?: $tenant->graphTenantId();
|
|
|
|
$clientId = config('graph.client_id');
|
|
$redirectUri = route('admin.rbac.callback');
|
|
|
|
abort_if(empty($clientId) || empty($redirectUri), Response::HTTP_BAD_GATEWAY, 'Graph client not configured');
|
|
|
|
$state = Str::uuid()->toString();
|
|
|
|
$request->session()->put('rbac_state', $state);
|
|
$request->session()->put('rbac_tenant', $tenant->getKey());
|
|
$returnTo = $this->sanitizeReturnPath(
|
|
$request->string('return')->toString()
|
|
?: route('filament.admin.resources.tenants.view', $tenant)
|
|
);
|
|
$request->session()->put('rbac_return', $returnTo);
|
|
|
|
$scopes = implode(' ', [
|
|
'openid',
|
|
'profile',
|
|
'offline_access',
|
|
'Directory.ReadWrite.All',
|
|
'Group.ReadWrite.All',
|
|
'DeviceManagementRBAC.ReadWrite.All',
|
|
'DeviceManagementConfiguration.ReadWrite.All',
|
|
]);
|
|
|
|
$url = "https://login.microsoftonline.com/{$targetTenant}/oauth2/v2.0/authorize?".http_build_query([
|
|
'client_id' => $clientId,
|
|
'response_type' => 'code',
|
|
'redirect_uri' => $redirectUri,
|
|
'response_mode' => 'query',
|
|
'scope' => $scopes,
|
|
'state' => $state,
|
|
]);
|
|
|
|
return redirect()->away($url);
|
|
}
|
|
|
|
public function callback(Request $request): RedirectResponse
|
|
{
|
|
$expectedState = $request->session()->pull('rbac_state');
|
|
$tenantId = $request->session()->pull('rbac_tenant');
|
|
$returnTo = $request->session()->pull('rbac_return');
|
|
|
|
abort_if(! $expectedState, Response::HTTP_FORBIDDEN, 'RBAC state missing');
|
|
abort_if($expectedState !== $request->string('state')->toString(), Response::HTTP_FORBIDDEN, 'Invalid RBAC state');
|
|
abort_if(! $tenantId, Response::HTTP_BAD_REQUEST, 'Tenant context missing');
|
|
|
|
/** @var Tenant $tenant */
|
|
$tenant = Tenant::query()->findOrFail($tenantId);
|
|
|
|
$code = $request->string('code')->toString();
|
|
abort_if(empty($code), Response::HTTP_BAD_REQUEST, 'Authorization code missing');
|
|
|
|
$tokens = $this->exchangeAuthorizationCode($code);
|
|
|
|
if (empty($tokens['access_token'])) {
|
|
abort(Response::HTTP_BAD_GATEWAY, 'Failed to exchange code for token');
|
|
}
|
|
|
|
$accessToken = $tokens['access_token'];
|
|
$ttl = CarbonImmutable::now()->addMinutes(5);
|
|
|
|
// Store token keyed by user id (preferred) and session id (fallback) to survive SPA refreshes.
|
|
if (auth()->check()) {
|
|
Cache::put($this->cacheKey($tenant, auth()->id(), null), $accessToken, $ttl);
|
|
}
|
|
|
|
Cache::put($this->cacheKey($tenant, auth()->id(), $request->session()->getId()), $accessToken, $ttl);
|
|
|
|
$destination = $this->sanitizeReturnPath($returnTo) ?: route('filament.admin.resources.tenants.view', $tenant);
|
|
|
|
return redirect()->to($destination);
|
|
}
|
|
|
|
/**
|
|
* @return array{access_token:?string,refresh_token:?string,expires_in:?int}
|
|
*/
|
|
private function exchangeAuthorizationCode(string $code): array
|
|
{
|
|
$response = Http::asForm()->post(sprintf(
|
|
'https://login.microsoftonline.com/%s/oauth2/v2.0/token',
|
|
config('graph.tenant_id', 'common')
|
|
), [
|
|
'client_id' => config('graph.client_id'),
|
|
'client_secret' => config('graph.client_secret'),
|
|
'code' => $code,
|
|
'grant_type' => 'authorization_code',
|
|
'redirect_uri' => route('admin.rbac.callback'),
|
|
]);
|
|
|
|
if ($response->failed()) {
|
|
return [];
|
|
}
|
|
|
|
$json = $response->json() ?: [];
|
|
|
|
return [
|
|
'access_token' => $json['access_token'] ?? null,
|
|
'refresh_token' => $json['refresh_token'] ?? null,
|
|
'expires_in' => $json['expires_in'] ?? null,
|
|
];
|
|
}
|
|
|
|
public static function cacheKey(Tenant $tenant, ?int $userId = null, ?string $sessionId = null): string
|
|
{
|
|
$suffix = $userId ? "user_{$userId}" : 'session_'.($sessionId ?: 'anon');
|
|
|
|
return sprintf('rbac_delegated_token_%s_%s', $tenant->getKey(), $suffix);
|
|
}
|
|
|
|
private function sanitizeReturnPath(?string $path): ?string
|
|
{
|
|
if (empty($path)) {
|
|
return null;
|
|
}
|
|
|
|
if (str_starts_with($path, 'http')) {
|
|
$parsed = parse_url($path);
|
|
$path = $parsed['path'] ?? '/';
|
|
}
|
|
|
|
return str_starts_with($path, '/') ? $path : '/';
|
|
}
|
|
}
|