Autenticação e autorização no Back-end com Slim Framework: JWT e políticas

Capítulo 11

Tempo estimado de leitura: 11 minutos

+ Exercício

Conceitos essenciais: autenticação vs. autorização

Autenticação responde “quem é você?” e normalmente resulta em uma identidade (ex.: id do usuário, e-mail, papéis) anexada ao request após validação de credenciais e/ou token.

Autorização responde “o que você pode fazer?” e é aplicada sobre uma identidade já autenticada, verificando papéis (roles) e/ou permissões (scopes/abilities) por meio de políticas (policies).

JWT na prática: estrutura, assinatura e expiração

JWT (JSON Web Token) é um token assinado composto por três partes: header.payload.signature. O servidor assina o token e o cliente o envia em cada requisição (geralmente no header Authorization: Bearer ...).

  • Header: algoritmo de assinatura (ex.: HS256 ou RS256) e tipo.
  • Payload (claims): dados e metadados. Claims comuns: sub (subject/usuário), exp (expiração), iat (emitido em), nbf (não antes de), iss (emissor), aud (audiência), jti (id do token).
  • Signature: garante integridade e autenticidade (não é criptografia do conteúdo).

Expiração: use exp curto para access tokens (ex.: 10–20 min). Para sessões mais longas, use refresh token (quando aplicável) com validade maior e rotação.

Assinatura: HS256 vs RS256

  • HS256 (HMAC): uma chave secreta compartilhada assina e valida. Simples, mas exige que o mesmo segredo esteja em todos os serviços que validam.
  • RS256 (RSA): chave privada assina, chave pública valida. Facilita distribuição segura para múltiplos serviços (somente a pública precisa ser compartilhada).

Rotação de chaves (conceito) e kid

Rotação de chaves reduz impacto de vazamentos e permite trocar chaves sem derrubar sessões imediatamente. Uma abordagem comum:

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

  • Manter um conjunto de chaves ativas (ex.: atual e anterior) para validação.
  • Assinar novos tokens com a chave atual.
  • Incluir no header do JWT um kid (Key ID) para indicar qual chave foi usada.
  • Após um período, remover a chave antiga quando tokens assinados por ela expirarem.

Modelo de endpoints: login, refresh e logout

Um desenho típico (ajuste ao seu domínio):

  • POST /auth/login: valida credenciais e emite access token (curto) e refresh token (longo, opcional).
  • POST /auth/refresh: troca refresh token válido por um novo par (rotação).
  • POST /auth/logout: invalida refresh token (revogação no servidor) quando aplicável.

JWT de access token costuma ser stateless (não precisa consultar banco a cada request), mas refresh token geralmente é stateful para permitir revogação/rotação (armazenado com hash no banco).

Passo a passo: emissão de JWT (access token)

Exemplo com a biblioteca firebase/php-jwt (instalação omitida). A ideia é centralizar a emissão em um serviço.

<?php

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

final class JwtIssuer
{
    public function __construct(
        private string $issuer,
        private string $audience,
        private string $privateKeyOrSecret,
        private string $alg,
        private int $accessTtlSeconds,
        private string $keyId // para rotacao (kid)
    ) {}

    public function issueAccessToken(array $identity, array $scopes = []): string
    {
        $now = time();

        $payload = [
            'iss' => $this->issuer,
            'aud' => $this->audience,
            'iat' => $now,
            'nbf' => $now,
            'exp' => $now + $this->accessTtlSeconds,
            'sub' => (string) $identity['id'],
            'email' => $identity['email'] ?? null,
            'roles' => $identity['roles'] ?? [],
            'scp' => $scopes,
        ];

        $headers = [
            'kid' => $this->keyId,
            'typ' => 'JWT',
        ];

        return JWT::encode($payload, $this->privateKeyOrSecret, $this->alg, null, $headers);
    }
}

Boas práticas de claims:

  • Evite colocar dados sensíveis no payload (JWT é apenas Base64URL, não é segredo).
  • Use sub como identificador principal do usuário.
  • Use roles e/ou scp (scopes) para autorização, mas considere que mudanças de permissão só terão efeito após expiração do token (ou com estratégia de revogação).

Refresh token (quando aplicável): rotação e revogação

Refresh token é usado para obter novos access tokens sem pedir login novamente. Para reduzir risco:

  • Rotação: a cada refresh, emita um novo refresh token e invalide o anterior.
  • Armazenamento: guarde apenas o hash do refresh token no servidor (como senha), com data de expiração e status.
  • Detecção de reutilização: se um refresh token já rotacionado for usado novamente, trate como possível roubo e revogue a família/sessão.

Exemplo de formato (alto nível) de tabela de refresh tokens:

campodescrição
ididentificador interno
user_iddono do token
token_hashhash do refresh token
expires_atvalidade
revoked_atrevogação (null se ativo)
replaced_byid do novo refresh token (rotação)
created_atauditoria

O access token pode continuar stateless; o refresh token é o ponto de controle de sessão.

Middleware de autenticação: validar token e anexar identidade ao request

O middleware deve:

  • Ler Authorization e extrair o Bearer token.
  • Validar assinatura, algoritmo esperado, iss/aud (se usados), e claims temporais (exp, nbf).
  • Anexar uma identidade normalizada ao request (ex.: atributo auth).
  • Responder 401 quando ausente/inválido/expirado.
<?php

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

final class JwtAuthMiddleware implements MiddlewareInterface
{
    public function __construct(
        private JwtKeyResolver $keyResolver, // resolve por kid
        private string $expectedAlg,
        private string $expectedIssuer,
        private string $expectedAudience,
        private ApiProblemResponder $responder
    ) {}

    public function process(Request $request, RequestHandlerInterface $handler): Response
    {
        $auth = $request->getHeaderLine('Authorization');
        if (!preg_match('/^Bearer\s+(\S+)$/', $auth, $m)) {
            return $this->responder->unauthorized('missing_token', 'Token Bearer ausente.');
        }

        $jwt = $m[1];

        try {
            $headers = JWT::jsonDecode(JWT::urlsafeB64Decode(explode('.', $jwt)[0] ?? ''));
            $kid = $headers->kid ?? null;

            $keyMaterial = $this->keyResolver->resolve($kid);
            $decoded = JWT::decode($jwt, new Key($keyMaterial, $this->expectedAlg));

            // Validacoes adicionais (iss/aud) - firebase/php-jwt nao valida automaticamente
            if (($decoded->iss ?? null) !== $this->expectedIssuer) {
                return $this->responder->unauthorized('invalid_issuer', 'Emissor do token invalido.');
            }
            if (($decoded->aud ?? null) !== $this->expectedAudience) {
                return $this->responder->unauthorized('invalid_audience', 'Audiencia do token invalida.');
            }

            $identity = [
                'userId' => $decoded->sub ?? null,
                'email'  => $decoded->email ?? null,
                'roles'  => $decoded->roles ?? [],
                'scopes' => $decoded->scp ?? [],
                'token'  => [
                    'iat' => $decoded->iat ?? null,
                    'exp' => $decoded->exp ?? null,
                    'kid' => $kid,
                ],
            ];

            if (empty($identity['userId'])) {
                return $this->responder->unauthorized('invalid_subject', 'Token sem subject (sub).');
            }

            return $handler->handle($request->withAttribute('auth', $identity));
        } catch (\Throwable $e) {
            // Nao vaze detalhes de excecao
            return $this->responder->unauthorized('invalid_token', 'Token invalido ou expirado.');
        }
    }
}

Resolver de chaves por kid (conceito aplicado)

Para rotação, o middleware não deve depender de uma única chave fixa. Um resolver simples pode buscar em configuração/cache:

<?php

final class JwtKeyResolver
{
    /** @param array<string,string> $keysByKid */
    public function __construct(private array $keysByKid, private string $defaultKid) {}

    public function resolve(?string $kid): string
    {
        $kid = $kid ?: $this->defaultKid;
        if (!isset($this->keysByKid[$kid])) {
            throw new \RuntimeException('Unknown kid');
        }
        return $this->keysByKid[$kid];
    }
}

Em produção, esse mapa pode vir de um endpoint JWKS interno, variável de ambiente, vault, ou cache distribuído. O ponto é: validar tokens emitidos por chaves antigas por um período.

Autorização por policies: papéis e permissões

Uma policy é uma regra explícita que decide se uma identidade pode executar uma ação. Evite “if role == admin” espalhado; centralize em policies reutilizáveis.

Modelo de policy

<?php

interface Policy
{
    /** @param array $auth identidade anexada no request */
    public function allows(array $auth, Request $request): bool;

    /** mensagem opcional para auditoria/log */
    public function message(): string;
}

final class RolePolicy implements Policy
{
    public function __construct(private string $requiredRole) {}

    public function allows(array $auth, Request $request): bool
    {
        $roles = $auth['roles'] ?? [];
        return in_array($this->requiredRole, $roles, true);
    }

    public function message(): string
    {
        return 'Role requerida: ' . $this->requiredRole;
    }
}

final class ScopePolicy implements Policy
{
    public function __construct(private string $requiredScope) {}

    public function allows(array $auth, Request $request): bool
    {
        $scopes = $auth['scopes'] ?? [];
        return in_array($this->requiredScope, $scopes, true);
    }

    public function message(): string
    {
        return 'Permissao requerida: ' . $this->requiredScope;
    }
}

Middleware de autorização (403) usando policies

Este middleware assume que a autenticação já anexou auth no request. Se não houver identidade, responda 401; se houver e a policy negar, responda 403.

<?php

final class AuthorizationMiddleware implements MiddlewareInterface
{
    public function __construct(private Policy $policy, private ApiProblemResponder $responder) {}

    public function process(Request $request, RequestHandlerInterface $handler): Response
    {
        $auth = $request->getAttribute('auth');
        if (!$auth) {
            return $this->responder->unauthorized('not_authenticated', 'Autenticacao necessaria.');
        }

        if (!$this->policy->allows($auth, $request)) {
            return $this->responder->forbidden('not_authorized', 'Acesso negado.');
        }

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

Aplicando autenticação e policies em rotas e grupos

Em Slim, a aplicação de middlewares em grupos ajuda a manter consistência. Um padrão comum:

  • Grupo /api com autenticação JWT.
  • Subgrupos por domínio com policies específicas.
<?php

$app->group('/api', function ($group) {
    $group->get('/me', MeAction::class);

    $group->group('/admin', function ($admin) {
        $admin->get('/users', AdminListUsersAction::class);
        $admin->post('/users', AdminCreateUserAction::class);
    })
    ->add(new AuthorizationMiddleware(new RolePolicy('admin'), $responder));

    $group->group('/billing', function ($billing) {
        $billing->post('/charge', CreateChargeAction::class);
    })
    ->add(new AuthorizationMiddleware(new ScopePolicy('billing:charge'), $responder));
})
->add(new JwtAuthMiddleware($keyResolver, 'RS256', 'https://seu-issuer', 'seu-aud', $responder));

Ordem importa: o middleware de autenticação deve rodar antes do de autorização (no exemplo, o grupo /api recebe autenticação e os subgrupos recebem autorização).

Respostas padronizadas para 401 e 403

Padronize o formato para facilitar consumo no front-end e observabilidade. Um formato simples e consistente:

{
  "error": {
    "status": 401,
    "code": "invalid_token",
    "message": "Token invalido ou expirado."
  }
}

Exemplo de responder (com headers úteis):

<?php

use Slim\Psr7\Response;

final class ApiProblemResponder
{
    public function unauthorized(string $code, string $message): Response
    {
        $payload = json_encode(['error' => ['status' => 401, 'code' => $code, 'message' => $message]]);
        $res = new Response(401);

        // Ajuda clientes a entenderem que precisa de Bearer
        return $res
            ->withHeader('Content-Type', 'application/json')
            ->withHeader('WWW-Authenticate', 'Bearer realm="api", error="' . $code . '"')
            ->withBody($this->stream($payload));
    }

    public function forbidden(string $code, string $message): Response
    {
        $payload = json_encode(['error' => ['status' => 403, 'code' => $code, 'message' => $message]]);
        $res = new Response(403);

        return $res
            ->withHeader('Content-Type', 'application/json')
            ->withBody($this->stream($payload));
    }

    private function stream(string $content)
    {
        $stream = fopen('php://temp', 'r+');
        fwrite($stream, $content);
        rewind($stream);
        return new \Slim\Psr7\Stream($stream);
    }
}

Quando usar 401: token ausente, inválido, expirado, assinatura inválida, iss/aud incorretos.

Quando usar 403: token válido, usuário autenticado, mas sem permissão/papel para a ação.

Cuidados com armazenamento do token no cliente

Onde guardar access token

  • Evite localStorage para tokens sensíveis em aplicações web, pois XSS pode exfiltrar o token.
  • Prefira cookies HttpOnly + Secure + SameSite quando o cliente é um browser e você controla o domínio. Isso reduz exposição a XSS (mas exige mitigação de CSRF).
  • Em apps mobile/desktop, use armazenamento seguro do sistema (Keychain/Keystore) quando disponível.

Refresh token e cookies

  • Se usar refresh token, considere armazená-lo em cookie HttpOnly e manter o access token em memória (variável) para reduzir persistência.
  • Se usar cookies, implemente proteção CSRF (ex.: double submit cookie ou token anti-CSRF) para endpoints sensíveis como /auth/refresh e operações de escrita.

Transporte e logs

  • Use HTTPS sempre.
  • Nunca registre o JWT completo em logs. Se precisar, registre apenas jti, sub, kid e timestamps.
  • Considere clock skew: permita pequena tolerância (ex.: 30–60s) ao validar nbf/exp dependendo da biblioteca/estratégia.

Checklist de segurança e consistência

  • Access token curto (exp baixo) + refresh token com rotação quando aplicável.
  • Validar algoritmo esperado (não aceitar “alg” arbitrário).
  • Validar iss e aud para evitar tokens de outros emissores.
  • Usar kid e resolver chaves para suportar rotação.
  • Middleware de autenticação anexa auth ao request; middleware de autorização aplica policies por rota/grupo.
  • Respostas 401/403 padronizadas e sem vazar detalhes internos.

Agora responda o exercício sobre o conteúdo:

Em um back-end com JWT, qual situação deve resultar em resposta 403 (Forbidden) em vez de 401 (Unauthorized)?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Use 401 quando não há autenticação válida (token ausente, inválido, expirado, iss/aud incorretos). Use 403 quando o usuário está autenticado, mas não tem autorização conforme a policy (roles/scopes).

Próximo capitúlo

Arquitetura Limpa com Slim Framework: camadas, limites e dependências

Arrow Right Icon
Capa do Ebook gratuito Back-end com Slim Framework (PHP): Rotas, Middlewares e Arquitetura Limpa
69%

Back-end com Slim Framework (PHP): Rotas, Middlewares e Arquitetura Limpa

Novo curso

16 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.