Authorization Middleware
Authorization middleware controls access based on OAuth scopes, user privileges, roles, and user types. These run after authentication to enforce fine-grained access control.
Middleware Aliases
Section titled “Middleware Aliases”| Alias | Class | Purpose |
|---|---|---|
scope | CheckForAnyScope | OAuth scope (any match) |
scopes | CheckScopes | OAuth scope (all required) |
privilege | Privilege | Login user privilege check |
advisor_role | CheckAdvisorRole | Advisor role + type validation |
user_type | UserType | User type restriction (advisor/client) |
can | Authorize | Policy-based authorization |
prevent_privileged_impersonation | PreventPrivilegedImpersonation | Block actions during impersonation |
1. CheckForAnyScope (scope)
Section titled “1. CheckForAnyScope (scope)”File: app/Http/Middleware/CheckForAnyScope.php
Extends Laravel Passport’s scope middleware. Validates that the OAuth token has at least one of the specified scopes.
Implementation
Section titled “Implementation”final class CheckForAnyScope extends \Laravel\Passport\Http\Middleware\CheckForAnyScope{ /** * Do not check the scope when using cookie authentication */ #[\Override] public function handle($request, $next, ...$scopes): Response { if (Str::isEmptyOrNull($request->bearerToken())) { return $next($request); }
return parent::handle($request, $next, ...$scopes); }}Key Behavior
Section titled “Key Behavior”- Extends Laravel Passport - inherits base scope checking logic
- Skips scope check for cookie auth - if no bearer token, proceeds without validation
- Delegates to parent for actual scope validation when bearer token exists
Route Usage
Section titled “Route Usage”// Any of these scopes grants accessRoute::middleware('scope:read-data,admin')->group(function () { Route::get('data', [DataController::class, 'index']);});
// First-party apps onlyRoute::middleware('scope:first-party')->group(function () { Route::get('sensitive', [SensitiveController::class, 'index']);});Common Scopes
Section titled “Common Scopes”| Scope | Description |
|---|---|
first-party | RightCapital’s own applications |
household:read | Read household data |
household:write | Modify household data |
advisor:read | Read advisor data |
advisor:write | Modify advisor data |
2. CheckScopes (scopes)
Section titled “2. CheckScopes (scopes)”File: app/Http/Middleware/CheckScopes.php
Extends Laravel Passport’s scope middleware. Validates that the OAuth token has all of the specified scopes.
Implementation
Section titled “Implementation”final class CheckScopes extends \Laravel\Passport\Http\Middleware\CheckScopes{ /** * Do not check the scope when using cookie authentication */ #[\Override] public function handle($request, $next, ...$scopes): Response { if (Str::isEmptyOrNull($request->bearerToken())) { return $next($request); }
return parent::handle($request, $next, ...$scopes); }}Key Behavior
Section titled “Key Behavior”- Same cookie-auth bypass logic as
CheckForAnyScope - Requires all specified scopes vs any
Route Usage
Section titled “Route Usage”// ALL scopes requiredRoute::middleware('scopes:household:read,household:write')->group(function () { Route::put('households/{household}', [HouseholdController::class, 'update']);});Difference from scope
Section titled “Difference from scope”| Middleware | Logic | Example |
|---|---|---|
scope:a,b | a OR b | Either scope grants access |
scopes:a,b | a AND b | Both scopes required |
3. Privilege
Section titled “3. Privilege”File: app/Http/Middleware/Privilege.php
Checks login user privileges (stored on advisor model, different from OAuth scopes).
Implementation
Section titled “Implementation”final class Privilege{ public function handle(Request $request, Closure $next, string $privilege): Response { $login_user = Auth::getUserOrFail();
if (($login_user->advisor->privileges[$privilege] ?? false) !== true) { throw new AccessDeniedHttpException('You don't have ' . str_replace('_', ' ', $privilege) . ' privilege.'); }
return $next($request); }}Key Behavior
Section titled “Key Behavior”- Takes single privilege per middleware invocation
- Uses
Auth::getUserOrFail()to get authenticated user - Checks
advisor->privileges[$privilege]array fortruevalue - Underscores in privilege name replaced with spaces in error message
Privilege Types
Section titled “Privilege Types”Privileges are stored as a JSON array on the advisor model:
// advisor->privileges = ['manage_users' => true, 'manage_billing' => true, ...]| Privilege | Description |
|---|---|
manage_users | User management access |
manage_billing | Billing and payment access |
manage_integrations | Third-party integration access |
view_reports | Report viewing access |
export_data | Data export access |
Route Usage
Section titled “Route Usage”Route::middleware('privilege:manage_users')->group(function () { Route::resource('users', UserController::class);});
// Chain multiple privileges (requires all)Route::middleware(['privilege:manage_billing', 'privilege:view_reports'])->group(function () { Route::get('billing-reports', [BillingReportController::class, 'index']);});Error Response
Section titled “Error Response”{ "message": "You don't have manage users privilege."}HTTP Status: 403 Forbidden
4. UserType
Section titled “4. UserType”File: app/Http/Middleware/UserType.php
Restricts access based on user type using enum comparison.
Implementation
Section titled “Implementation”final class UserType{ public function handle(Request $request, Closure $next, string $user_type): Response { if ($request->user() !== null) { $type = $request->user()->type;
if ($type !== \RightCapital\Core\Enums\User\UserType::from($user_type)) { throw new AccessDeniedHttpException('You are not ' . Str::indefiniteArticle($user_type) . " $user_type."); } } else { throw new AuthenticationException('You must log in first.'); }
return $next($request); }}Key Behavior
Section titled “Key Behavior”- Uses
RightCapital\Core\Enums\User\UserTypeenum for comparison - Converts middleware parameter to enum via
UserType::from($user_type) - Uses
Str::indefiniteArticle()for grammatically correct error (“a” vs “an”)
Route Usage
Section titled “Route Usage”// Advisor-only routesRoute::middleware('user_type:advisor')->group(function () { Route::resource('households', HouseholdController::class); Route::resource('clients', ClientController::class);});
// Client-only routesRoute::middleware('user_type:client')->group(function () { Route::get('my-plan', [ClientPlanController::class, 'show']); Route::put('my-profile', [ClientProfileController::class, 'update']);});Error Responses
Section titled “Error Responses”// Not an advisor{ "message": "You are not an advisor."}
// Not a client{ "message": "You are not a client."}
// Not logged in{ "message": "You must log in first."}HTTP Status: 403 Forbidden (user type mismatch) or 401 Unauthorized (not logged in)
5. CheckAdvisorRole
Section titled “5. CheckAdvisorRole”File: app/Http/Middleware/CheckAdvisorRole.php
Validates advisor role and organization type via database query.
Implementation
Section titled “Implementation”final class CheckAdvisorRole{ public function handle(Request $request, Closure $next, string ...$roles_and_types): Response { $login_user = Auth::getUserOrFail();
if ($login_user->advisor === null) { throw new AccessDeniedHttpException('You don't have the permission.'); }
$advisor_role_query = AdvisorRole::where(AdvisorRole::COLUMN_ADVISOR_ID, $login_user->advisor->id); $check_roles = [];
$advisor_role_query->whereNested(function (Builder $query) use ($roles_and_types, &$check_roles): void { foreach ($roles_and_types as $role_with_type) { [$role, $type] = explode('|', $role_with_type); $query->orWhere(function (Builder $query) use ($role, $type): void { $query->where(AdvisorRole::COLUMN_ROLE, $role)->where(AdvisorRole::COLUMN_TYPE, $type); }); $check_roles[] = $role; } });
if (!$advisor_role_query->exists()) { throw new AccessDeniedHttpException('You don't have the ' . implode(' or ', $check_roles) . ' permissions.'); }
return $next($request); }}Key Behavior
Section titled “Key Behavior”- Database query on
AdvisorRoletable (not in-memory check) - Parameter format:
ROLE|TYPE- both role and type are required - Uses nested OR conditions - any matching role/type combination grants access
- Uses
Auth::getUserOrFail()to get login user
Parameter Format
Section titled “Parameter Format”advisor_role:ROLE|TYPE,ROLE|TYPE,...Both ROLE and TYPE are required in each parameter:
- ROLE:
SUPER_ADMIN,ADMIN,MEMBER,VIEWER - TYPE:
ORGANIZATION,FEDERATION,ENTERPRISE
Route Usage
Section titled “Route Usage”// Organization super admin onlyRoute::middleware('advisor_role:SUPER_ADMIN|ORGANIZATION')->group(function () { Route::resource('organization/settings', OrgSettingsController::class);});
// Federation admin OR organization adminRoute::middleware('advisor_role:ADMIN|FEDERATION,ADMIN|ORGANIZATION')->group(function () { Route::get('admin/dashboard', [AdminDashboardController::class, 'index']);});Error Responses
Section titled “Error Responses”// Not an advisor{ "message": "You don't have the permission."}
// No matching role{ "message": "You don't have the ADMIN or SUPER_ADMIN permissions."}HTTP Status: 403 Forbidden
6. Authorize (can)
Section titled “6. Authorize (can)”File: Laravel’s built-in Illuminate\Auth\Middleware\Authorize
Uses Laravel’s policy system for model-based authorization.
Route Usage
Section titled “Route Usage”// Check policy ability on route modelRoute::put('households/{household}', [HouseholdController::class, 'update']) ->middleware('can:update,household');
// Check global abilityRoute::post('exports', [ExportController::class, 'store']) ->middleware('can:create-export');
// Check with additional parameterRoute::post('households/{household}/reports', [ReportController::class, 'store']) ->middleware('can:create-report,household');Policy Example
Section titled “Policy Example”class HouseholdPolicy{ public function update(User $user, Household $household): bool { return $user->advisor_id === $household->advisor_id; }
public function createReport(User $user, Household $household): bool { return $this->update($user, $household) && $user->hasFeature('reports'); }}7. PreventPrivilegedImpersonation
Section titled “7. PreventPrivilegedImpersonation”File: app/Http/Middleware/PreventPrivilegedImpersonation.php
Blocks all actions when an employee is impersonating a user.
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 session keys for impersonation state (not
app('impersonator')) - Two types of impersonation detected:
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)
Route Usage
Section titled “Route Usage”Route::middleware(['auth', 'prevent_privileged_impersonation'])->group(function () { // WebAuthn registration (security sensitive) Route::post('webauthn/registration/initialize', ...); Route::post('webauthn/registration/finalize', ...);
// Password change Route::put('password', [PasswordController::class, 'update']);
// API key management Route::resource('api-keys', ApiKeyController::class);});Error Response
Section titled “Error Response”{ "message": "This action cannot be performed while impersonating."}HTTP Status: 403 Forbidden
See [impersonation.md](../impersonation/ for more details.
Combining Middleware
Section titled “Combining Middleware”Authorization middleware is typically combined in layers:
Route::group([ 'middleware' => ['auth:web', 'scope:first-party']], function () { // Level 1: Authenticated + First-party scope
Route::group(['middleware' => 'user_type:advisor'], function () { // Level 2: Advisor users only
Route::group(['middleware' => 'advisor_role:ADMIN|ORGANIZATION'], function () { // Level 3: Organization admins only
Route::group(['middleware' => 'prevent_privileged_impersonation'], function () { // Level 4: Not during impersonation Route::post('sensitive-action', [SensitiveController::class, 'store']); }); }); });});Authorization Decision Flow
Section titled “Authorization Decision Flow”Request │ ▼┌─────────────────┐│ OAuth Scope? │────No────▶ (skip if cookie auth)│ (bearer token) │└─────────────────┘ │ Yes ▼┌─────────────────┐│ User Type? │────No────▶ 403└─────────────────┘ │ Yes ▼┌─────────────────┐│ Advisor Role? │────No────▶ 403│ (DB query) │└─────────────────┘ │ Yes ▼┌─────────────────┐│ Privilege? │────No────▶ 403└─────────────────┘ │ Yes ▼┌─────────────────┐│ Policy Check? │────No────▶ 403└─────────────────┘ │ Yes ▼┌─────────────────┐│ Impersonation │────Yes───▶ 403│ Session Keys? │└─────────────────┘ │ No ▼ ContinueSummary
Section titled “Summary”| Middleware | Logic | Use Case |
|---|---|---|
scope:a,b | OR | OAuth scope check (any), skips for cookie auth |
scopes:a,b | AND | OAuth scope check (all), skips for cookie auth |
privilege:p | Single | User privilege check (advisor->privileges) |
user_type:advisor | - | Advisor-only routes (enum comparison) |
user_type:client | - | Client-only routes (enum comparison) |
advisor_role:R|T | OR | Role + org type check (DB query) |
can:ability,model | - | Policy-based auth |
prevent_privileged_impersonation | - | Block if session has impersonation keys |