Skip to content

Impersonation Middleware

Impersonation allows RightCapital employees to access advisor accounts for support purposes. Multiple middleware components work together to control and audit this capability.

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

File: app/Http/Middleware/CheckAzureAd.php

Authenticates employees using Azure AD tokens and validates permissions.

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);
}
}
  • Uses RightCapital\LaravelAzureAd\AzureAd facade 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 AzureAdUnauthorizedException for invalid tokens
  • Throws AccessDeniedHttpException for missing permissions
// Basic Azure AD authentication
Route::middleware('azure_ad')->group(function () {
Route::post('impersonate/{advisor}', [ImpersonateController::class, 'store']);
});
// With specific permission requirement
Route::middleware('azure_ad:user:impersonate')->group(function () {
Route::post('impersonate/{advisor}', [ImpersonateController::class, 'store']);
});
// 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(Attribute::TARGET_CLASS|Attribute::IS_REPEATABLE)]
final class RequiresImpersonatorEmployeePermission
{
public function __construct(public string $action, public AzureAdPermission $azure_ad_permission)
{
}
}
  • 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
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
}
}

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

File: app/Http/Middleware/CheckImpersonatorEmployeePermission.php

Middleware that checks controller attributes and validates employee permissions during impersonation.

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);
}
}
  • Gets employee from Session key SessionEntity::KEY_IMPERSONATOR_EMPLOYEE (not app('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’s action parameter
  • Checks $attribute->azure_ad_permission->value against $employee['permissions'] array

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
]
{
"message": "You don't have permission to perform this operation."
}

HTTP Status: 403 Forbidden


File: app/Http/Middleware/PreventPrivilegedImpersonation.php

Blocks security-sensitive actions during ANY impersonation session.

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);
}
}
  • Checks two session keys for impersonation state:
    • SessionEntity::KEY_IMPERSONATOR_EMPLOYEE - Employee impersonation
    • SessionEntity::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::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']);
});
{
"message": "This action cannot be performed while impersonating."
}

HTTP Status: 403 Forbidden


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

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 Action

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) │
└────────────────────────────────────┘
Continue

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