Authentication Middleware
Retail-API supports multiple authentication mechanisms for different client types: web sessions, OAuth bearer tokens, internal service tokens (via HTTP Basic Auth), and Azure AD for employee access.
Middleware Aliases
Section titled “Middleware Aliases”| Alias | Class | Purpose |
|---|---|---|
auth | Authenticate | Session or bearer token auth |
auth.basic | AuthenticateWithBasicAuth | HTTP Basic auth |
auth.internal | AuthenticateInternalApplication | Internal service auth (HTTP Basic) |
guest | Unauthenticated | Requires NOT logged in |
azure_ad | CheckAzureAd | Azure AD token validation |
verified | Laravel’s EnsureEmailIsVerified | Email verification check |
1. Authenticate (Primary Auth)
Section titled “1. Authenticate (Primary Auth)”File: app/Http/Middleware/Authenticate.php
The primary authentication middleware that routes between session-based and token-based authentication.
Implementation
Section titled “Implementation”class Authenticate extends Middleware{ public function handle($request, Closure $next, ...$guards): Response { if (Str::isEmptyOrNull($request->bearerToken()) && in_array('web', $guards, true)) { $this->authenticate($request, ['web']);
// Check if session has been kicked using Redis $session_service = app(RedisSessionService::class); $session = $session_service->findOrFail(\Session::getId());
if ($session->status === SessionStatus::KICKED) { Cookie::queue(Cookie::forget(config('session.cookie'))); throw new KickedException('You have been kicked and must log in again.'); } } else { $this->authenticate($request, ['api']); }
return $next($request); }
protected function authenticate($request, array $guards): void { try { parent::authenticate($request, $guards); } catch (AuthenticationException $e) { throw new AuthenticationException( 'You must log in first.', $e->guards(), $e->redirectTo($request) ); } }}Authentication Logic
Section titled “Authentication Logic”- If no bearer token AND
webguard requested → use session auth - Otherwise → use API (bearer token) auth
- For web sessions, check if session was kicked via
RedisSessionService
Session Kicked Check
Section titled “Session Kicked Check”Sessions can be marked as KICKED in Redis, forcing logout:
if ($session->status === SessionStatus::KICKED) { Cookie::queue(Cookie::forget(config('session.cookie'))); throw new KickedException('You have been kicked and must log in again.');}Route Usage
Section titled “Route Usage”// Standard auth (web or api)Route::middleware('auth')->group(function () { Route::get('profile', [ProfileController::class, 'show']);});
// Specific guardRoute::middleware('auth:web')->group(function () { Route::get('dashboard', [DashboardController::class, 'index']);});Error Response
Section titled “Error Response”{ "message": "You must log in first."}HTTP Status: 401 Unauthorized
2. Unauthenticated (Guest)
Section titled “2. Unauthenticated (Guest)”File: app/Http/Middleware/Unauthenticated.php
Ensures the user is NOT authenticated. Used for login, registration, and password reset routes.
Implementation
Section titled “Implementation”public function handle(Request $request, Closure $next, string|null $guard = null): SymfonyResponse{ if (Auth::guard($guard)->check()) { abort(Response::HTTP_FORBIDDEN, 'You are already logged in.'); }
return $next($request);}Route Usage
Section titled “Route Usage”Route::middleware('guest')->group(function () { // Login Route::post('sessions', [SessionController::class, 'store']);
// Registration Route::post('users', [UserController::class, 'store']);
// Password reset Route::post('password/email', [PasswordController::class, 'sendResetLink']); Route::post('password/reset', [PasswordController::class, 'reset']);
// OTP verification Route::post('otp/verify', [OtpController::class, 'verify']) ->middleware('throttle:otp-verify');});Error Response
Section titled “Error Response”{ "message": "You are already logged in."}HTTP Status: 403 Forbidden
3. AuthenticateInternalApplication
Section titled “3. AuthenticateInternalApplication”File: app/Http/Middleware/AuthenticateInternalApplication.php
Authenticates requests from internal services using HTTP Basic Authentication.
Implementation
Section titled “Implementation”final class AuthenticateInternalApplication{ public function handle(Request $request, Closure $next, string ...$applications): Response { $application = $request->getUser();
if (!in_array($application, $applications, true)) { throw new AccessDeniedHttpException("The request application[$application] is invalid."); }
if (Str::isEmptyOrNull($request->getPassword()) || $request->getPassword() !== config('internal-auth.api_key.' . $application)) { throw new AccessDeniedHttpException("You don't have the [$application] permission."); }
return $next($request); }}Authentication Method
Section titled “Authentication Method”Uses HTTP Basic Authentication:
- Username = Application name (e.g.,
Admin,Morningstar) - Password = API key from config
Request Format
Section titled “Request Format”# Using curl with Basic Authcurl -u "Admin:secret_api_key" https://api.example.com/internal/endpoint
# Or with Authorization headercurl -H "Authorization: Basic QWRtaW46c2VjcmV0X2FwaV9rZXk=" https://api.example.com/internal/endpointConfiguration
Section titled “Configuration”return [ 'api_key' => [ 'Admin' => env('INTERNAL_API_KEY_ADMIN'), 'Morningstar' => env('INTERNAL_API_KEY_MORNINGSTAR'), 'DataPlatform' => env('INTERNAL_API_KEY_DATA_PLATFORM'), 'Calculation' => env('INTERNAL_API_KEY_CALCULATION'), 'Scheduler' => env('INTERNAL_API_KEY_SCHEDULER'), ],];Route Usage
Section titled “Route Usage”// Single serviceRoute::middleware('auth.internal:DataPlatform')->group(function () { Route::get('advisors', [AdvisorController::class, 'index']);});
// Multiple services allowedRoute::middleware('auth.internal:Admin,Morningstar')->group(function () { Route::post('cache', [CacheController::class, 'clear']);});Error Responses
Section titled “Error Responses”// Invalid application{ "message": "The request application[Unknown] is invalid."}
// Invalid API key{ "message": "You don't have the [Admin] permission."}HTTP Status: 403 Forbidden
4. CheckAzureAd
Section titled “4. CheckAzureAd”File: app/Http/Middleware/CheckAzureAd.php
Validates Azure AD tokens for employee access, optionally checking 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 Features
Section titled “Key Features”- Skips validation for OPTIONS (CORS preflight) requests
- Uses
AzureAd::check()facade for token validation - Supports permission-based access control via middleware parameters
- Throws
AzureAdUnauthorizedExceptionfor invalid tokens
Route Usage
Section titled “Route Usage”// Basic Azure AD validationRoute::middleware('azure_ad')->group(function () { Route::post('impersonate/{advisor}', [ImpersonateController::class, 'store']);});
// With permission requirementRoute::middleware('azure_ad:Admin.ReadWrite')->group(function () { Route::post('admin/action', [AdminController::class, 'execute']);});
// Multiple permissions (any match)Route::middleware('azure_ad:User.Read,User.ReadWrite')->group(function () { Route::get('users', [UserController::class, 'index']);});Error Responses
Section titled “Error Responses”// Invalid token{ "message": "Unauthorized"}
// Missing permission{ "message": "You don't have permission to perform this operation, please contact the corporate directory administrator."}HTTP Status: 401 Unauthorized or 403 Forbidden
5. EnsureEmailIsVerified
Section titled “5. EnsureEmailIsVerified”File: Laravel’s built-in Illuminate\Auth\Middleware\EnsureEmailIsVerified
Ensures the authenticated user has verified their email address.
Route Usage
Section titled “Route Usage”Route::middleware(['auth', 'verified'])->group(function () { // Only accessible after email verification Route::get('sensitive-data', [SensitiveController::class, 'index']);});Error Response
Section titled “Error Response”{ "message": "Your email address is not verified."}HTTP Status: 403 Forbidden
Authentication Flow Diagram
Section titled “Authentication Flow Diagram”Request │ ▼┌─────────────────────────────┐│ Has Bearer Token? │└─────────────────────────────┘ │ │ Yes No (+ 'web' guard) │ │ ▼ ▼┌────────┐ ┌────────────┐│ API │ │ Session ││ Guard │ │ Guard │└────────┘ └────────────┘ │ ▼ ┌─────────────────────┐ │ Session Kicked? │ │ (RedisSessionService) └─────────────────────┘ │ │ Yes No │ │ ▼ ▼ ┌────────┐ ┌────────────┐ │ Clear │ │ Continue │ │ Cookie │ │ Request │ │ + Throw│ └────────────┘ │ Kicked │ │ Exception └────────┘OAuth Scopes
Section titled “OAuth Scopes”For OAuth-protected routes, combine auth with scope middleware:
Route::middleware(['auth:api', 'scope:read-data'])->group(function () { Route::get('data', [DataController::class, 'index']);});See [authorization.md](../authorization/ for scope middleware details.
Summary
Section titled “Summary”| Middleware | Use Case | Error Code |
|---|---|---|
auth | General authentication | 401 |
auth:web | Web session only | 401 |
auth:api | Bearer token only | 401 |
guest | Login/registration pages | 403 |
auth.internal | Service-to-service (HTTP Basic) | 403 |
azure_ad | Azure AD employee access | 401/403 |
verified | Email verified users | 403 |