Tokens de API e abilities
Tokens Bearer mad_api_* (tabela mad_api_token): emissão por artisan mad:api-token ou pela tela /app/tokens-api com reveal único, abilities descobertas da route table, TTL, revogação soft.
Todo acesso a uma rota mad.api passa por um token Bearer da tabela mad_api_token.
O token nao e so uma credencial: ele carrega o escopo. Cada token e amarrado a um par
user + unit, e a unit determina o tenant — e, em multi-database, o banco onde as queries
daquela requisicao vao rodar. Nao existe "escolher a empresa" no cabecalho: quem emitiu o
token ja decidiu.
Formato e armazenamento
O token em claro e mad_api_ + 48 caracteres hex (bin2hex(random_bytes(24))), gerado por
MadApiTokenService::issue(). O banco guarda so o sha256 (token_hash) — o claro nunca
persiste e nunca e logado. Perdeu, nao ha recuperacao: revoga e emite outro.
| Coluna | Conteudo |
|---|---|
token_hash |
hash('sha256', $plain) — o unico material sensivel gravado. |
token_prefix |
mad_api_ — so para exibicao/identificacao na listagem. |
user_id / unit_id |
Escopo fixo do token (dono + unidade). |
active |
'1' / '0' — mesmo contrato do mad_mcp_token. |
abilities |
JSON array de permissoes finas; NULL = acesso total. |
expires_at |
TTL; resolve() rejeita vencido. |
revoked_at |
Soft-revoke — a linha fica para auditoria. |
last_used_at |
Atualizado no uso, com granularidade de 300s (nao escreve a cada request). |
Emitindo pela linha de comando
php artisan mad:api-token {login} {unit}
| Argumento / flag | Descricao |
|---|---|
login |
Login do usuario dono do token (obrigatorio). |
unit |
Id da unit — escopo fixo do token; obrigatorio ao emitir. |
--name= |
Label do token (ex.: "integracao ERP X"). |
--abilities= |
CSV de permissoes finas ("orders.read,reports.*"); vazio = acesso total. |
--days= |
TTL em dias; default config('mad.api.token_ttl_days'). 0 = sem expiracao. |
--list |
Lista os tokens ativos do usuario (metadados, sem hash). |
--revoke= |
Revoga (soft) um token do usuario por id. |
php artisan mad:api-token admin 1 \
--name="integracao ERP X" \
--abilities="orders.read,orders.write" \
--days=90
# imprime o claro UMA vez: mad_api_3f9c...
php artisan mad:api-token admin --list
php artisan mad:api-token admin --revoke=7
O TTL default vem de config('mad.api.token_ttl_days') — MAD_API_TOKEN_TTL_DAYS, 365 dias
de fabrica. 0 emite token sem expiracao.
Emitindo pela tela admin
A tela Tokens de API vive em /app/tokens-api (ApiTokenList, control-plane iam: o
admin ve todos os tokens do install). O botao de emissao abre o drawer ApiTokenForm, que
delega ao mesmo MadApiTokenService::issue() — as validacoes fail-closed sao unicas, nao ha
caminho alternativo.
Duas coisas dessa tela merecem atencao:
- Reveal unico. O token em claro aparece uma so vez, num painel com botao de copiar. Fechou o drawer, o claro se perdeu de proposito — nao ha "mostrar de novo".
- Checklist de abilities descoberta das rotas. As opcoes nao sao digitadas a mao: vem de
MadApiTokenService::declaredAbilities(), que varre a route table extraindo os parametros demad.api:<a>,<b>e mostra, ao lado de cada ability, as rotas que a exigem (GET /api/orders). Ability que nenhuma rota declara nao aparece para ser concedida.
A revogacao na listagem e soft (active = '0' + revoked_at): a linha fica para
auditoria e o acesso cai na hora, porque o middleware revalida o token a cada requisicao.
Abilities
A rota declara o que exige; o token declara o que cobre. A checagem e
MadApiTokenService::tokenCan(), fail-closed no miss:
| Abilities do token | Semantica |
|---|---|
null ou [] |
Acesso total — passa em qualquer exigencia. |
['*'] |
Acesso total. |
['orders.read'] |
Match exato. |
['orders.*'] |
Wildcard de prefixo: cobre orders.read, orders.x.y. |
| Qualquer outra | 403, citando a ability que faltou. |
Na rota, virgula significa todas exigidas:
Route::middleware('mad.api:orders.read')->get('/orders', ...);
Route::middleware('mad.api:orders.write,orders.approve')->post('/orders/{order}/approve', ...);
Formato aceito na emissao (normalizeAbilities(): trim + lowercase + dedup): segmentos
[a-z0-9_-] separados por ponto, com .* opcional no fim — orders.read, reports.* — ou
* sozinho. Formato invalido lanca RuntimeException na emissao, nao passa silenciosamente.
Falha de ability nao conta como falha de auth no rate-limit: o token e legitimo, so nao cobre aquela rota.
Ciclo de vida e revalidacao
resolve() so devolve token active = '1' e nao vencido. Alem disso, o middleware revalida o
vinculo inteiro a cada requisicao — user ativo, unit ativa, unit pertencente ao usuario,
tenant ativo. Desativar o usuario no IAM ou tirar a unit dele corta o token imediatamente,
sem precisar revoga-lo.
Erros do lado do token, todos JSON:
| Status | Quando |
|---|---|
401 |
Sem Bearer, ou token inexistente / expirado / revogado. Header WWW-Authenticate: Bearer. |
403 |
Escopo invalido na revalidacao, ou token sem a ability exigida pela rota. |
429 |
20 falhas de auth por IP em 600s (ApiRateLimiter); Retry-After: 600. |
Boas praticas
- Um token por integracao, com
--namedescritivo: revogar uma integracao nao pode derrubar as outras. - Abilities minimas. Token sem abilities e acesso total; e conveniente para um smoke local e ruim para producao.
- TTL curto para o que e temporario.
--days=7para uma carga pontual;0(sem expiracao) so quando ha rotina de revogacao. - Nomes de ability sao contrato. Renomear a ability na rota invalida os tokens ja emitidos que a listavam — trate como versao de API.
Proximos passos
- Autenticacao — o pipeline completo do middleware
mad.api. - Declarando rotas — onde a ability e declarada.
- Exemplos praticos — o mesmo recurso via Bearer, em curl.