Autenticação
mad.api (Bearer mad_api_*, stateless, escopo tenant/unit do token + abilities por rota), mad.auth (sessão + CSRF) para o frontend logado, e HMAC no Driver REST.
Existem duas portas de entrada para os endpoints JSON deste app, e a diferença entre elas é ter ou não sessão:
| Middleware | Prova de identidade | Arquivo de rotas | Para quem |
|---|---|---|---|
mad.api |
Authorization: Bearer mad_api_… — stateless, sem cookie, sem CSRF. |
routes/api.php (prefixo /api, grupo api do Laravel). |
Integração externa: outro sistema, script, job, app mobile sem login web. |
mad.auth |
Cookie de sessão do admin + X-CSRF-TOKEN na escrita. |
routes/modules/*.php (grupo web, com ->prefix('api')). |
O seu frontend já logado no admin (AJAX da própria aplicação). |
Nos dois casos o controller é o mesmo Mad\Rest\ApiResourceController — muda só
o middleware da rota.
mad.api — Bearer stateless com escopo de tenant
mad.api é o alias registrado pelo MadServiceProvider para
App\Http\Middleware\MadApiTenantMiddleware (classe app-level, como os models
IAM que BelongsToTenant referencia):
// packages/mad-framework/src/mad/MadServiceProvider.php
$router->aliasMiddleware('mad.api', \App\Http\Middleware\MadApiTenantMiddleware::class);
Ele faz autenticação e escopo num passo só, porque o escopo já vem no token: cada token de API é amarrado a um par user + unit, e a unit determina o tenant — e, em multi-database, o banco. Ordem do que o middleware faz por requisição:
- Limpa o estado da requisição anterior (contexts estáticos, conexão, session in-memory).
- Rate limit por IP:
ApiRateLimiterbloqueia após 20 falhas de auth numa janela de 600s →429comRetry-After: 600. - Lê
$request->bearerToken(); ausente ou inválido/expirado/revogado →401comWWW-Authenticate: Bearer. - Revalida o vínculo a cada requisição (fail-closed): user existe e ativo, unit existe e ativa, unit pertence ao usuário, tenant existe e ativo → senão
403. - Checa as abilities exigidas pela rota (veja abaixo) →
403se faltar alguma. - Seta
TenantContext/UnitContext(é o que fazBelongsToTenant/BelongsToUnitfiltrarem e carimbarem) e a identidade em session in-memory para oHasMadAuditcarimbarcreated_by/userid/userunitid. - Em multi-database, repointa o data-plane para a conexão do tenant (
TenantConnectionResolver::applyTenant()); tenant semconnection_namecai emresetToDefault(), igual ao login web.
curl 'https://app.exemplo.com/api/orders' \
-H 'Authorization: Bearer mad_api_3f9c…' \
-H 'Accept: application/json'
As rotas de routes/api.php rodam no grupo api do Laravel:
sem StartSession, sem CSRF. A session usada para a identidade de
auditoria é in-memory e é apagada em dois pontos (início do
handle() e no terminate()) — defesa em profundidade contra
worker long-lived / Octane, o mesmo padrão do SetTenantConnection.
Abilities — authz fino declarado na rota
O parâmetro do middleware é a lista de permissões que a rota exige. Vírgula significa todas exigidas (E lógico, não OU):
// routes/api.php
Route::middleware('mad.api')->apiResource('orders', OrderApiController::class); // só auth + tenant
Route::middleware('mad.api:orders.read')->get('/orders', ...); // exige orders.read
Route::middleware('mad.api:orders.read,orders.write')->post('/orders', ...); // exige AS DUAS
A checagem é MadApiTokenService::tokenCan(), fail-closed no miss:
| Abilities do token | Semântica |
|---|---|
null ou [] | Acesso total — passa em qualquer exigência. |
['*'] | Acesso total. |
['orders.read'] | Match exato. |
['orders.*'] | Wildcard de prefixo: cobre orders.read, orders.x.y. |
| qualquer outra | 403, com a ability que faltou na mensagem. |
Falha de ability não conta como falha de auth no rate-limit — o token é legítimo, só não cobre aquela rota.
Essas declarações são a fonte única da checklist de abilities na tela de
emissão de tokens: a tela varre a route table procurando mad.api:<ability>.
Declarou na rota, aparece na tela. Veja
Tokens de API e abilities.
Quem é o chamador dentro do controller
O middleware deixa a identidade resolvida em atributos do request:
$user = $request->attributes->get('mad_api_user'); // App\Models\Iam\User
$token = $request->attributes->get('mad_api_token'); // App\Models\Api\Token
O mad.api deliberadamente não carrega programs/actions do IAM (custa N
queries por requisição). Ele resolve auth + escopo + abilities; autorização por
ação/registro é do authorize() do controller. Ability de rota e
permissão de tela são eixos diferentes — não confie numa esperando a outra.
mad.auth — a mesma porta de entrada do admin
Para endpoints JSON consumidos pelo seu próprio frontend já logado, a rota continua
vivendo em routes/modules/*.php dentro do grupo web — sessão,
cookies e CSRF iguais a qualquer tela do admin.
mad.auth é o alias registrado pelo MadServiceProvider para
Mad\Http\Middleware\MadAuthenticate:
// packages/mad-framework/src/mad/MadServiceProvider.php
$router->aliasMiddleware('mad.auth', \Mad\Http\Middleware\MadAuthenticate::class);
// routes/modules/admin.php
Route::middleware('mad.auth')
->prefix('api')
->group(function () {
Route::apiResource('import-templates', \App\Http\Controllers\Sys\ImportTemplateApiController::class);
});
O middleware verifica PermissionGate::isLogged() — a mesma checagem de sessão
usada por qualquer rota /app/*. Sem sessão válida: 401 com corpo
JSON (a rota é tratada como wire/AJAX pelo middleware, então a negação já vem em JSON, não
como redirect HTML):
401 Unauthorized
{ "error": "Permission denied", "redirect": "/app/login" }
Diferente das telas (mad.auth + mad.permission), os endpoints
Route::apiResource deste repo usam só mad.auth —
não passam pelo gate de permissão de programa/ação por padrão. Se o recurso precisa de
autorização mais fina (por ação, por registro, por papel), implemente no hook
authorize() do controller:
protected function authorize(string $action, Request $request): void
{
if (! $request->user()?->can($action, $this->model)) {
abort(403);
}
}
POST/PUT/DELETE
Como essas rotas estão dentro do grupo web, elas não
estão na lista de exceção de
$middleware->validateCsrfTokens(except: [...]) em
bootstrap/app.php (só embed/v1/* e
agent-console/v1/* são exceções, e são stateless por Bearer). Isso
significa que um cliente JSON que já tem sessão (mesmo browser, mesma aba do admin)
ainda precisa mandar o token CSRF em toda escrita — header X-CSRF-TOKEN
com o valor de csrf_token() (o mesmo que o layout do app já injeta num
<meta> para o mad-livewire.js ler) — exatamente como
qualquer outro POST autenticado deste app. Um cliente puramente
externo (sem cookie de sessão do app) não consegue chamar essas rotas — e é esse
exatamente o ponto: rota atrás de mad.auth é para o seu
frontend logado. Integração externa entra por mad.api (Bearer, sem
sessão e sem CSRF), em routes/api.php.
Terceira superfície: Driver REST (HMAC)
mad.api expõe recursos modelados. Quando o consumidor precisa operar o
banco do app — o caso do Database Manager do MadBuilder — a peça é outra: o
Driver REST (Mad\Rest\RestDriver*), autenticado por
HMAC simétrico, não por token de usuário:
| Característica | Route::apiResource | Driver REST |
|---|---|---|
| Identidade | Usuário logado (sessão) | Chave pareada (key_id + segredo de 256 bits) |
| Transporte da prova | Cookie de sessão + X-CSRF-TOKEN | Headers X-Mad-* assinados por requisição |
| Habilitado por padrão? | Sim, junto com o app | Não — fail-closed, requer MAD_REST_DRIVER_ENABLED=true |
| O que expõe | Um recurso modelado (Eloquent, validado, projetado) | SQL cru contra a conexão pareada (ping/introspect/run/update/plan) |
| Onde mora | routes/modules/*.php, prefixo api | config('mad.rest_driver.path'), default _mad/rest-driver |
Habilitar e parear uma chave:
php artisan mad:rest-driver:install --connection=business --read-only
O comando gera key_id + segredo, persiste em storage/app/mad/rest-driver.json (gitignored, chmod 600) e imprime o segredo uma única vez — quem consome cola esses dados na conexão REST do MadBuilder.
Como cada requisição é assinada
Toda chamada ao Driver REST leva quatro headers; o middleware RestDriverHmac
valida em ordem fail-closed antes de deixar o request passar:
| Header | Conteúdo |
|---|---|
X-Mad-Key | key_id da chave pareada. |
X-Mad-Timestamp | Unix timestamp do request (janela de 300s contra replay). |
X-Mad-Nonce | Valor aleatório, único por request (cache anti-replay por chave+nonce). |
X-Mad-Signature | hash_hmac('sha256', canonical, secret) — comparado com hash_equals (timing-safe). |
requestCanonical = "madrest-req\n" . keyId . "\n" . action . "\n" . ts . "\n" . nonce . "\n" . sha256hex(body)
responseCanonical = "madrest-res\n" . keyId . "\n" . ts . "\n" . nonce . "\n" . sha256hex(body)
A resposta também é assinada (mesmos headers, secret simétrico) — quem
chama confirma que a resposta realmente veio do app pareado, não de um proxy no meio do
caminho. Falha em qualquer checagem (chave desconhecida, timestamp fora da janela, nonce
repetido, assinatura inválida) devolve sempre o mesmo 401 genérico, sem
revelar qual checagem caiu.
Rate limit por chave (config('mad.rest_driver.rate_limit'), default 120
req/min) via RateLimiter::for('mad-rest-driver', ...), aplicado com
throttle:mad-rest-driver nas rotas do driver.
Ele executa SQL contra a conexão pareada (ping/introspect/
run/update/plan) — não modela um recurso,
não valida por Model::rules(), não tem projeção/$appends.
É a ponte do MadBuilder Database Manager para operar o banco do app sem VPN, não um
substituto do ApiResourceController. Para expor dados ao seu próprio
frontend, use Route::apiResource com mad.auth.
Próximos passos
- Tokens de API e abilities — emitir, revogar e restringir tokens
mad_api_*. - Declarando rotas — onde
mad.api/mad.authentram na declaração da rota. - CSRF protection — detalhe do
@csrf/X-CSRF-TOKENque toda escrita autenticada precisa. - Middleware stack — onde
mad.authse encaixa no pipeline completo.