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.:
HS256ouRS256) 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:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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
subcomo identificador principal do usuário. - Use
rolese/ouscp(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:
| campo | descrição |
|---|---|
| id | identificador interno |
| user_id | dono do token |
| token_hash | hash do refresh token |
| expires_at | validade |
| revoked_at | revogação (null se ativo) |
| replaced_by | id do novo refresh token (rotação) |
| created_at | auditoria |
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
Authorizatione 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
401quando 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
/apicom 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/refreshe operações de escrita.
Transporte e logs
- Use HTTPS sempre.
- Nunca registre o JWT completo em logs. Se precisar, registre apenas
jti,sub,kide timestamps. - Considere clock skew: permita pequena tolerância (ex.: 30–60s) ao validar
nbf/expdependendo da biblioteca/estratégia.
Checklist de segurança e consistência
- Access token curto (
expbaixo) + refresh token com rotação quando aplicável. - Validar algoritmo esperado (não aceitar “alg” arbitrário).
- Validar
isseaudpara evitar tokens de outros emissores. - Usar
kide resolver chaves para suportar rotação. - Middleware de autenticação anexa
authao request; middleware de autorização aplica policies por rota/grupo. - Respostas 401/403 padronizadas e sem vazar detalhes internos.