Impersonation Middleware
Impersonation allows RightCapital employees to access advisor accounts for support purposes. Multiple middleware components work together to control and audit this capability.
Components
Section titled “Components”| Component | Location | Purpose |
|---|---|---|
| CheckAzureAd | app/Http/Middleware/ | Azure AD employee authentication |
| CheckImpersonatorEmployeePermission | app/Http/Middleware/ | Attribute-based permission check |
| PreventPrivilegedImpersonation | app/Http/Middleware/ | Block sensitive actions during impersonation |
| RequiresImpersonatorEmployeePermission | app/Http/Middleware/Attributes/ | PHP 8 attribute for permission declaration |
1. CheckAzureAd
Section titled “1. CheckAzureAd”File: app/Http/Middleware/CheckAzureAd.php
Authenticates employees using Azure AD tokens and validates permissions.
Implementation
Section titled “Implementation”class CheckAzureAd{ public function handle(Request $request, Closure $next, string ...$permissions): Response { if ($request->method() !== Request::METHOD_OPTIONS) { try { AzureAd::check(); } catch (InvalidTokenException $e) { throw new AzureAdUnauthorizedException('', 0, $e); }
if (count($permissions) > 0 && count(array_intersect($permissions, AzureAd::getClaim(AzureAdService::CLAIM_PERMISSIONS, []))) === 0) { throw new AccessDeniedHttpException('You don't have permission to perform this operation, please contact the corporate directory administrator.'); } }
return $next($request); }}Key Behavior
Section titled “Key Behavior”- Uses
RightCapital\LaravelAzureAd\AzureAdfacade for token validation - Skips OPTIONS requests (CORS preflight)
- Accepts optional permission parameters to check against token claims
- Uses
AzureAd::getClaim(AzureAdService::CLAIM_PERMISSIONS, [])to get permissions from token - Throws
AzureAdUnauthorizedExceptionfor invalid tokens - Throws
AccessDeniedHttpExceptionfor missing permissions
Route Usage
Section titled “Route Usage”// Basic Azure AD authenticationRoute::middleware('azure_ad')->group(function () { Route::post('impersonate/{advisor}', [ImpersonateController::class, 'store']);});
// With specific permission requirementRoute::middleware('azure_ad:user:impersonate')->group(function () { Route::post('impersonate/{advisor}', [ImpersonateController::class, 'store']);});Error Responses
Section titled “Error Responses”// Invalid token{ "message": "Unauthorized"}HTTP Status: 401 Unauthorized
// Missing permission{ "message": "You don't have permission to perform this operation, please contact the corporate directory administrator."}HTTP Status: 403 Forbidden
2. RequiresImpersonatorEmployeePermission Attribute
Section titled “2. RequiresImpersonatorEmployeePermission Attribute”File: app/Http/Middleware/Attributes/RequiresImpersonatorEmployeePermission.php
PHP 8 attribute for declaring required Azure AD permissions on controller actions.
Attribute Definition
Section titled “Attribute Definition”#[Attribute(Attribute::TARGET_CLASS|Attribute::IS_REPEATABLE)]final class RequiresImpersonatorEmployeePermission{ public function __construct(public string $action, public AzureAdPermission $azure_ad_permission) { }}Key Behavior
Section titled “Key Behavior”- CLASS-level only (not method-level) - applies to controller class
- IS_REPEATABLE - multiple attributes can be applied to same class
- Takes
action(string) to match controller method name - Takes
azure_ad_permission(enum) for permission requirement
Usage in Controllers
Section titled “Usage in Controllers”use App\Http\Middleware\Attributes\RequiresImpersonatorEmployeePermission;use App\Support\Enums\AzureAdPermission;
#[RequiresImpersonatorEmployeePermission(action: 'store', azure_ad_permission: AzureAdPermission::HOUSEHOLD_CREATE)]#[RequiresImpersonatorEmployeePermission(action: 'impersonate', azure_ad_permission: AzureAdPermission::USER_IMPERSONATE)]class ImpersonationController extends Controller{ public function store(Request $request) { // Requires HOUSEHOLD_CREATE permission when impersonating }
public function impersonate(Advisor $advisor) { // Requires USER_IMPERSONATE permission when impersonating }}3. AzureAdPermission Enum
Section titled “3. AzureAdPermission Enum”File: app/Support/Enums/AzureAdPermission.php
Defines available Azure AD permissions.
enum AzureAdPermission: string{ use GetsBackedEnumValues;
case USER_IMPERSONATE = 'user:impersonate'; case HOUSEHOLD_CREATE = 'household:create'; case WEBSITE_VISITOR = 'WebsiteVisitor';}| Case | Value | Description |
|---|---|---|
USER_IMPERSONATE | user:impersonate | Permission to impersonate users |
HOUSEHOLD_CREATE | household:create | Permission to create households while impersonating |
WEBSITE_VISITOR | WebsiteVisitor | Website visitor access |
4. CheckImpersonatorEmployeePermission
Section titled “4. CheckImpersonatorEmployeePermission”File: app/Http/Middleware/CheckImpersonatorEmployeePermission.php
Middleware that checks controller attributes and validates employee permissions during impersonation.
Implementation
Section titled “Implementation”final class CheckImpersonatorEmployeePermission{ public function handle(Request $request, Closure $next): Response { $route = $request->route(); $employee = Session::get(SessionEntity::KEY_IMPERSONATOR_EMPLOYEE); /** @var class-string<\App\Http\Controllers\Controller> $controller_fqcn */ $controller_fqcn = $route?->getControllerClass();
if ($route === null || $employee === null || $controller_fqcn === null) { return $next($request); }
$controller_reflection = new ReflectionClass($controller_fqcn);
foreach ($controller_reflection->getAttributes(RequiresImpersonatorEmployeePermission::class) as $reflection_attribute) { $requires_impersonator_employee_permission_attribute = $reflection_attribute->newInstance();
if ($route->getActionMethod() === $requires_impersonator_employee_permission_attribute->action && !in_array($requires_impersonator_employee_permission_attribute->azure_ad_permission->value, $employee['permissions'], true)) { throw new AccessDeniedHttpException('You don't have permission to perform this operation.'); } }
return $next($request); }}Key Behavior
Section titled “Key Behavior”- Gets employee from Session key
SessionEntity::KEY_IMPERSONATOR_EMPLOYEE(notapp('impersonator')) - Only checks if impersonating - skips if
$employee === null - Gets controller class via
$route->getControllerClass() - Uses ReflectionClass to read class-level attributes
- Matches
$route->getActionMethod()against attribute’sactionparameter - Checks
$attribute->azure_ad_permission->valueagainst$employee['permissions']array
Employee Session Data
Section titled “Employee Session Data”The employee is stored in session as an array:
Session::get(SessionEntity::KEY_IMPERSONATOR_EMPLOYEE);// Returns:[ 'email' => 'support@rightcapital.com', 'permissions' => ['user:impersonate', 'household:create'], // ... other employee data]Error Response
Section titled “Error Response”{ "message": "You don't have permission to perform this operation."}HTTP Status: 403 Forbidden
5. PreventPrivilegedImpersonation
Section titled “5. PreventPrivilegedImpersonation”File: app/Http/Middleware/PreventPrivilegedImpersonation.php
Blocks security-sensitive actions during ANY impersonation session.
Implementation
Section titled “Implementation”final class PreventPrivilegedImpersonation{ public function handle(Request $request, Closure $next): Response { if (Session::get(SessionEntity::KEY_IMPERSONATOR_EMPLOYEE) !== null || Session::get(SessionEntity::KEY_ADMIN_PORTAL_IMPERSONATOR_USER_ID) !== null) { throw new AccessDeniedHttpException('This action cannot be performed while impersonating.'); }
return $next($request); }}Key Behavior
Section titled “Key Behavior”- Checks two session keys for impersonation state:
SessionEntity::KEY_IMPERSONATOR_EMPLOYEE- Employee impersonationSessionEntity::KEY_ADMIN_PORTAL_IMPERSONATOR_USER_ID- Admin portal impersonation
- Blocks ALL routes with this middleware during impersonation (no route name checking)
- Does NOT use
app('impersonator')or check specific actions
Route Usage
Section titled “Route Usage”Route::middleware(['auth', 'prevent_privileged_impersonation'])->group(function () { // WebAuthn registration (security sensitive) Route::post('webauthn/registration/initialize', [WebAuthnController::class, 'initializeRegistration']); Route::post('webauthn/registration/finalize', [WebAuthnController::class, 'finalizeRegistration']);
// Password management Route::put('password', [PasswordController::class, 'update']);
// API key management Route::resource('api-keys', ApiKeyController::class)->only(['store', 'destroy']);});Error Response
Section titled “Error Response”{ "message": "This action cannot be performed while impersonating."}HTTP Status: 403 Forbidden
6. Impersonation Session Keys
Section titled “6. Impersonation Session Keys”Two session keys control impersonation state:
| Key | Source | Purpose |
|---|---|---|
SessionEntity::KEY_IMPERSONATOR_EMPLOYEE | Employee portal | Stores employee data array during impersonation |
SessionEntity::KEY_ADMIN_PORTAL_IMPERSONATOR_USER_ID | Admin portal | Stores admin user ID during impersonation |
7. Impersonation Flow
Section titled “7. Impersonation Flow”Employee Login (Azure AD) │ ▼┌────────────────────────┐│ CheckAzureAd ││ - Validate AD token ││ - Check permissions │└────────────────────────┘ │ ▼┌────────────────────────┐│ Start Impersonation ││ POST /impersonate/{advisor}│ - Store employee in Session└────────────────────────┘ │ ▼┌─────────────────────────────────────────┐│ CheckImpersonatorEmployeePermission ││ - Check Session::get(KEY_IMPERSONATOR) ││ - Read #[RequiresImpersonatorEmployee] ││ - Match action name to route method ││ - Validate permission in employee array │└─────────────────────────────────────────┘ │ ▼┌────────────────────────────────────┐│ PreventPrivilegedImpersonation ││ - Check Session impersonation keys ││ - Block if ANY impersonation active└────────────────────────────────────┘ │ ▼ Controller Action8. Middleware Stack
Section titled “8. Middleware Stack”Request │ ▼┌────────────────────────┐│ CheckAzureAd │────Invalid Token────▶ 401│ (azure_ad middleware) │────No Permission────▶ 403└────────────────────────┘ │ ▼┌────────────────────────────────────────────┐│ CheckImpersonatorEmployeePermission │────Missing Permission────▶ 403│ (global middleware, checks Session key) │└────────────────────────────────────────────┘ │ ▼┌────────────────────────────────────┐│ PreventPrivilegedImpersonation │────Impersonating────▶ 403│ (route middleware, checks Session) │└────────────────────────────────────┘ │ ▼ ContinueSummary
Section titled “Summary”| Component | Purpose | Check Method | Error |
|---|---|---|---|
CheckAzureAd | Azure AD token + permissions | AzureAd::check() + getClaim() | 401/403 |
#[RequiresImpersonatorEmployeePermission] | Declare action permissions | Class-level, IS_REPEATABLE | - |
CheckImpersonatorEmployeePermission | Validate employee permissions | Session key + ReflectionClass | 403 |
PreventPrivilegedImpersonation | Block sensitive actions | Two Session keys | 403 |