0

Auth middleware — protecting routes

Intermediate5 min read·fw-09-004

Concept

Auth middleware protects routes by verifying the current request is authenticated before the controller runs. It's a guard in the middleware pipeline.

How auth middleware works:

  1. Resolve the current guard (session or token, based on configuration).
  2. Check if the request is authenticated via $guard->check().
  3. If YES: call $next($request) — continue to the next middleware/controller.
  4. If NO: return 401 Unauthorized (API) or redirect to login (web).

Guard resolution: Different route groups use different guards. API routes use the token guard. Web routes use the session guard. The middleware knows which guard to use via configuration or a constructor argument.

auth vs auth:api: Named middleware with parameters. auth uses the default (web) guard. auth:api uses the api guard explicitly.

Passing user to downstream handlers: After verifying authentication, the auth middleware can attach the user to the request: $request = $request->withAttribute('user', $guard->user()). Controllers then access $request->getAttribute('user') instead of calling the auth system.

Redirecting vs responding 401: For web (browser) requests, redirect to the login page. For API (JSON) requests, return a 401 JSON response. Detect via Accept: application/json header or route group.

Code Example

php
<?php
namespace Framework\Auth\Middleware;

use Framework\Auth\AuthManagerInterface;
use Framework\Http\Request;
use Framework\Http\Response;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;

class Authenticate implements MiddlewareInterface
{
    public function __construct(
        private readonly AuthManagerInterface $auth,
        private readonly string $guard = 'web',
    ) {}

    public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
    {
        $guard = $this->auth->guard($this->guard);

        if ($guard->guest()) {
            return $this->unauthenticated($request);
        }

        // Attach user to request for downstream handlers
        $request = $request->withAttribute('user', $guard->user());
        $request = $request->withAttribute('auth',  $guard);

        return $next->handle($request);
    }

    private function unauthenticated(ServerRequestInterface $request): ResponseInterface
    {
        // API requests → 401 JSON
        if ($this->expectsJson($request)) {
            return new \Nyholm\Psr7\Response(
                401,
                ['Content-Type' => 'application/json'],
                json_encode(['error' => 'Unauthenticated.'])
            );
        }

        // Web requests → redirect to login
        return new \Nyholm\Psr7\Response(302, ['Location' => '/login']);
    }

    private function expectsJson(ServerRequestInterface $request): bool
    {
        $accept = $request->getHeaderLine('Accept');
        return str_contains($accept, 'application/json') || str_contains($accept, 'application/vnd.api');
    }
}

// Using in a middleware with named guard
class AuthenticateWithGuard implements MiddlewareInterface
{
    public function __construct(
        private readonly AuthManagerInterface $auth,
        private readonly string $guardName,
    ) {}

    public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
    {
        // Same as above but uses $this->guardName
        $guard = $this->auth->guard($this->guardName);
        if ($guard->guest()) {
            return new \Nyholm\Psr7\Response(401, [], 'Unauthorized');
        }
        return $next->handle($request->withAttribute('user', $guard->user()));
    }
}

// Controller using the injected user
class UserController
{
    public function me(ServerRequestInterface $request): ResponseInterface
    {
        $user = $request->getAttribute('user'); // set by auth middleware
        return new \Framework\Http\JsonResponse($user->toArray());
    }
}