Authentication (OAuth2 + Sanctum)
Como o fluxo client_credentials funciona e como o token emitido é validado depois
Por que client_credentials, e não outro grant
OAuth2 define vários “grant types” — formas diferentes de trocar uma credencial por um token — para situações diferentes. authorization_code existe para quando há um usuário humano aprovando acesso via navegador (é o “faça login com o Google” clássico). Aqui não há usuário: quem se autentica é um software rodando na infraestrutura de um cliente, sem interface, sem sessão de navegador. client_credentials (RFC 6749 §4.4) é o grant desenhado exatamente para isso — machine-to-machine, sem etapa de autorização por humano: o software apresenta um client_id/client_secret e recebe um token de volta, ponto.
O par client_id/client_secret: o SerialKey
Esse serviço usa o SerialKey (formato LIC-{lookup}-{secret}, ver concepts) como esse par: o lookup (8 caracteres) faz o papel de client_id, o secret (24 caracteres) faz o papel de client_secret. client_id costuma ser considerado público (identifica quem está pedindo, não precisa ser segredo); client_secret é o que prova que quem está pedindo é quem diz ser.
O pipeline de emissão
IssueAccessTokenAction roda a emissão como um Pipeline do Laravel — uma sequência fixa de estágios, cada um podendo interromper o fluxo lançando exceção:
// app-modules/oauth/src/Providers/OauthServiceProvider.php
$this->app->bind(IssuesAccessTokens::class, fn ($app): IssueAccessTokenAction => new IssueAccessTokenAction(
$app->make(Pipeline::class),
[
FindActiveCredentialStage::class,
VerifySecretStage::class,
MatchDocumentStage::class,
AssertTenantActiveStage::class,
TouchCredentialStage::class,
IssueSanctumTokenStage::class,
],
));
Cada estágio faz uma verificação e só ela:
// FindActiveCredentialStage.php
$license = License::query()->active()->where('serial_lookup', $ctx->request->clientId)->with('tenant')->first();
if (! $license instanceof License) {
throw new InvalidClientCredentialsException;
}
// VerifySecretStage.php
if (! $this->hashVerifier->verify($ctx->request->clientSecret, $ctx->license->key_hash)) {
throw new InvalidClientCredentialsException;
}
FindActiveCredentialStage já filtra por active() — uma licença suspensa ou cancelada nem chega a VerifySecretStage. VerifySecretStage compara o segredo recebido contra o hash salvo (key_hash), nunca contra um valor em texto claro — o segredo real não existe em lugar nenhum do banco, só o hash SHA-256 dele (ver concepts). Os estágios seguintes (não mostrados aqui) checam o CNPJ informado contra o tenant da licença e se o tenant está ativo, antes de finalmente emitir o token:
// IssueSanctumTokenStage.php
$token = $license->createToken(
name: "{$prefix}:{$license->serial_lookup}",
abilities: [$scope], // license:read
expiresAt: Carbon::now()->addSeconds($lifetime), // 86400s = 24h
);
Separar em estágios é o que faz cada motivo de falha logável e testável isoladamente (app-modules/oauth/tests/Unit/Pipeline/*, um teste por estágio) — em vez de um método único com vários if aninhados decidindo o mesmo conjunto de coisas.
Sanctum: o que o token emitido realmente é
createToken() é do Laravel Sanctum. Sanctum gera um token opaco (uma string aleatória, sem estrutura decodificável como um JWT) e salva o hash dele numa tabela (personal_access_tokens) — o valor em texto claro ($token->plainTextToken) só existe no momento da criação, devolvido uma vez na resposta, nunca mais recuperável depois. Cada request autenticado subsequente manda esse token no header Authorization: Bearer <token>; o Sanctum recebe, faz hash do que chegou e busca esse hash na tabela — se bater, autentica o portador como a License dona do token (License implementa HasApiTokens, é ela quem fica “logada” nas requisições seguintes, não um usuário humano).
abilities: [$scope] (license:read) é o escopo do token — Sanctum permite checar isso via $request->user()->tokenCan('license:read') em qualquer rota, embora este serviço hoje só tenha uma habilidade e uma rota que a usa (GET /api/v1/license). Token expira em 24h (config('license.access_token.lifetime_seconds')); depois disso, o software cliente repete o client_credentials grant para conseguir um novo — não há refresh token neste fluxo.