Skip to content

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.

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

File: app/Http/Middleware/Authenticate.php

The primary authentication middleware that routes between session-based and token-based authentication.

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)
);
}
}
}
  1. If no bearer token AND web guard requested → use session auth
  2. Otherwise → use API (bearer token) auth
  3. For web sessions, check if session was kicked via RedisSessionService

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.');
}
// Standard auth (web or api)
Route::middleware('auth')->group(function () {
Route::get('profile', [ProfileController::class, 'show']);
});
// Specific guard
Route::middleware('auth:web')->group(function () {
Route::get('dashboard', [DashboardController::class, 'index']);
});
{
"message": "You must log in first."
}

HTTP Status: 401 Unauthorized


File: app/Http/Middleware/Unauthenticated.php

Ensures the user is NOT authenticated. Used for login, registration, and password reset routes.

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::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');
});
{
"message": "You are already logged in."
}

HTTP Status: 403 Forbidden


File: app/Http/Middleware/AuthenticateInternalApplication.php

Authenticates requests from internal services using HTTP Basic Authentication.

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);
}
}

Uses HTTP Basic Authentication:

  • Username = Application name (e.g., Admin, Morningstar)
  • Password = API key from config
Terminal window
# Using curl with Basic Auth
curl -u "Admin:secret_api_key" https://api.example.com/internal/endpoint
# Or with Authorization header
curl -H "Authorization: Basic QWRtaW46c2VjcmV0X2FwaV9rZXk=" https://api.example.com/internal/endpoint
config/internal-auth.php
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'),
],
];
// Single service
Route::middleware('auth.internal:DataPlatform')->group(function () {
Route::get('advisors', [AdvisorController::class, 'index']);
});
// Multiple services allowed
Route::middleware('auth.internal:Admin,Morningstar')->group(function () {
Route::post('cache', [CacheController::class, 'clear']);
});
// Invalid application
{
"message": "The request application[Unknown] is invalid."
}
// Invalid API key
{
"message": "You don't have the [Admin] permission."
}

HTTP Status: 403 Forbidden


File: app/Http/Middleware/CheckAzureAd.php

Validates Azure AD tokens for employee access, optionally checking 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);
}
}
  • Skips validation for OPTIONS (CORS preflight) requests
  • Uses AzureAd::check() facade for token validation
  • Supports permission-based access control via middleware parameters
  • Throws AzureAdUnauthorizedException for invalid tokens
// Basic Azure AD validation
Route::middleware('azure_ad')->group(function () {
Route::post('impersonate/{advisor}', [ImpersonateController::class, 'store']);
});
// With permission requirement
Route::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']);
});
// 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


File: Laravel’s built-in Illuminate\Auth\Middleware\EnsureEmailIsVerified

Ensures the authenticated user has verified their email address.

Route::middleware(['auth', 'verified'])->group(function () {
// Only accessible after email verification
Route::get('sensitive-data', [SensitiveController::class, 'index']);
});
{
"message": "Your email address is not verified."
}

HTTP Status: 403 Forbidden


Request
┌─────────────────────────────┐
│ Has Bearer Token? │
└─────────────────────────────┘
│ │
Yes No (+ 'web' guard)
│ │
▼ ▼
┌────────┐ ┌────────────┐
│ API │ │ Session │
│ Guard │ │ Guard │
└────────┘ └────────────┘
┌─────────────────────┐
│ Session Kicked? │
│ (RedisSessionService)
└─────────────────────┘
│ │
Yes No
│ │
▼ ▼
┌────────┐ ┌────────────┐
│ Clear │ │ Continue │
│ Cookie │ │ Request │
│ + Throw│ └────────────┘
│ Kicked │
│ Exception
└────────┘

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.


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