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.

ComponentLocationPurpose
CheckAzureAdapp/Http/Middleware/Azure AD employee authentication
CheckImpersonatorEmployeePermissionapp/Http/Middleware/Attribute-based permission check
PreventPrivilegedImpersonationapp/Http/Middleware/Block sensitive actions during impersonation
RequiresImpersonatorEmployeePermissionapp/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';
}
CaseValueDescription
USER_IMPERSONATEuser:impersonatePermission to impersonate users
HOUSEHOLD_CREATEhousehold:createPermission to create households while impersonating
WEBSITE_VISITORWebsiteVisitorWebsite 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:

KeySourcePurpose
SessionEntity::KEY_IMPERSONATOR_EMPLOYEEEmployee portalStores employee data array during impersonation
SessionEntity::KEY_ADMIN_PORTAL_IMPERSONATOR_USER_IDAdmin portalStores 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

ComponentPurposeCheck MethodError
CheckAzureAdAzure AD token + permissionsAzureAd::check() + getClaim()401/403
#[RequiresImpersonatorEmployeePermission]Declare action permissionsClass-level, IS_REPEATABLE-
CheckImpersonatorEmployeePermissionValidate employee permissionsSession key + ReflectionClass403
PreventPrivilegedImpersonationBlock sensitive actionsTwo Session keys403