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.

AliasClassPurpose
authAuthenticateSession or bearer token auth
auth.basicAuthenticateWithBasicAuthHTTP Basic auth
auth.internalAuthenticateInternalApplicationInternal service auth (HTTP Basic)
guestUnauthenticatedRequires NOT logged in
azure_adCheckAzureAdAzure AD token validation
verifiedLaravel’s EnsureEmailIsVerifiedEmail 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.


MiddlewareUse CaseError Code
authGeneral authentication401
auth:webWeb session only401
auth:apiBearer token only401
guestLogin/registration pages403
auth.internalService-to-service (HTTP Basic)403
azure_adAzure AD employee access401/403
verifiedEmail verified users403