Docs›REST API›Autenticação
REST API

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:

MiddlewareProva de identidadeArquivo de rotasPara 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:

  1. Limpa o estado da requisição anterior (contexts estáticos, conexão, session in-memory).
  2. Rate limit por IP: ApiRateLimiter bloqueia após 20 falhas de auth numa janela de 600s → 429 com Retry-After: 600.
  3. Lê $request->bearerToken(); ausente ou inválido/expirado/revogado → 401 com WWW-Authenticate: Bearer.
  4. 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.
  5. Checa as abilities exigidas pela rota (veja abaixo) → 403 se faltar alguma.
  6. Seta TenantContext/UnitContext (é o que faz BelongsToTenant/BelongsToUnit filtrarem e carimbarem) e a identidade em session in-memory para o HasMadAudit carimbar created_by/userid/userunitid.
  7. Em multi-database, repointa o data-plane para a conexão do tenant (TenantConnectionResolver::applyTenant()); tenant sem connection_name cai em resetToDefault(), igual ao login web.
curl 'https://app.exemplo.com/api/orders' \
  -H 'Authorization: Bearer mad_api_3f9c…' \
  -H 'Accept: application/json'
Stateless de verdade — nada persiste

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 tokenSemâ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 outra403, 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
Permissão de programa está fora do contrato

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);
    }
}
Sessão implica CSRF — também em 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ísticaRoute::apiResourceDriver REST
IdentidadeUsuário logado (sessão)Chave pareada (key_id + segredo de 256 bits)
Transporte da provaCookie de sessão + X-CSRF-TOKENHeaders X-Mad-* assinados por requisição
Habilitado por padrão?Sim, junto com o appNão — fail-closed, requer MAD_REST_DRIVER_ENABLED=true
O que expõeUm recurso modelado (Eloquent, validado, projetado)SQL cru contra a conexão pareada (ping/introspect/run/update/plan)
Onde moraroutes/modules/*.php, prefixo apiconfig('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:

HeaderConteúdo
X-Mad-Keykey_id da chave pareada.
X-Mad-TimestampUnix timestamp do request (janela de 300s contra replay).
X-Mad-NonceValor aleatório, único por request (cache anti-replay por chave+nonce).
X-Mad-Signaturehash_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.

Driver REST não é para o seu frontend

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