Spike AUTH-14.1 — claims app-only Entra e origem do 403¶
PLANID: AUTH-14.1 · Issue: #51
Data: 2026-08-30
Escopo: classificar origem do 403 reportado pelo cliente; contrato de claims; matriz HTTP gateway × backend.
1. Contexto¶
Cliente em ECOSIF_API_TOKEN_MODE=AZURE_ENTERPRISE_GATEWAY obtém access token via client_credentials e recebe 403 ao chamar APIs eCosif. Gateway provavelmente valida assinatura/iss/aud; o eCosif antes de AUTH-14.3 não resolvia identidade de aplicação.
2. Matriz de origem HTTP (como distinguir)¶
| Camada | HTTP típico | Corpo | Quando |
|---|---|---|---|
| API Gateway (APIM / AWS JWT authorizer) | 401 ou 403 | JSON/XML da plataforma (não ErrorResponse eCosif) |
aud/iss inválido, assinatura, policy exige scp, allowlist appid |
| Backend — falha JWT (decoder) | 401 | Spring OAuth2 / WWW-Authenticate |
Token malformado, expirado, assinatura inválida no Java |
| Backend — sem vínculo (AUTH-14.3+) | 401 | ErrorResponse JSON: code=AUTH_IDP_NOT_LINKED ou AUTH_UNAUTHORIZED, message contém identity_provider_link |
Token app-only válido criptograficamente, mas sem registro AZURE_ENTERPRISE_APP |
| Backend — app-only desligado | 401 | Idem | ECOSIF_ENABLE_AZURE_APP_ONLY=false e token classificado como app-only |
| Backend — role/client id | 401 | Idem (BadJwtException na validação) | Role ausente ou appid fora da allowlist |
| Backend — autorização negócio | 403 | Varia por serviço | Usuário técnico autenticado, mas sem ACL empresa/filial no endpoint |
Hipótese principal para o 403 do cliente (pré-AUTH-14)¶
| Evidência | Conclusão |
|---|---|
| Modo B humano documentado mapeia “usuário não encontrado” como problema de link, não 403 no starter | Falha de identidade no Java → 401, não 403 |
Gateway APIM pode usar failed-validation-httpcode="403" em policy customizada |
403 provável na borda se token não passa policy (ex.: exige scp) |
| Cliente ainda não enviou payload jwt.ms | Classificação definitiva gateway vs backend permanece pendente confirmação cliente (item 1 do plano) |
Procedimento ops (sem token completo):
- Comparar corpo da resposta: se não contém
"code":"AUTH_IDP_NOT_LINKED"→ provável gateway. - Chamar API direto no container Java (bypass gateway) com o mesmo Bearer: se 401 + JSON eCosif → confirma backend (identidade).
- Log gateway vs log
ecosif.auth.m2m outcome=denied(AUTH-14.7): presença do segundo indica request chegou ao backend.
3. Contrato de claims — token Entra v2 app-only¶
Referência: Microsoft identity platform access tokens · grant client_credentials · scope api://{resource}/.default.
Claims esperadas (obrigatórias / usadas pelo eCosif)¶
| Claim | Presente em app-only? | Uso eCosif |
|---|---|---|
iss |
Sim | JWKS / issuer validator |
aud |
Sim | = ECOSIF_AZURE_API_AUDIENCE (api://…) |
exp / nbf |
Sim | Validade |
tid |
Sim | Tenant (ECOSIF_AZURE_TENANT_ID) |
roles |
Sim (App role consentida) | ECOSIF_AZURE_APP_REQUIRED_ROLES |
appid |
Sim (v1 style) ou via azp |
Allowlist + fallback lookup |
azp |
Frequentemente | Allowlist + fallback lookup |
oid |
Sim (object id do service principal) | Lookup preferencial em identity_provider_link |
idtyp |
Frequentemente app |
Detecção app-only (AzureEntraAppOnlySupport) |
scp |
Não (delegated) | Ausência confirma app-only |
sub |
Sim (often = oid do SP) | Não usar como chave primária B2B (diferente de humano) |
Payload sintético de referência (homolog / testes)¶
Valores fictícios — não são secrets; usar só em jwt.ms local ou testes unitários.
{
"aud": "api://00000000-0000-4000-8000-000000000001",
"iss": "https://login.microsoftonline.com/11111111-1111-4111-8111-111111111111/v2.0",
"iat": 1756550000,
"nbf": 1756550000,
"exp": 1756553600,
"aio": "XXXX",
"appid": "22222222-2222-4222-8222-222222222222",
"appidacr": "1",
"idtyp": "app",
"oid": "33333333-3333-4333-8333-333333333333",
"roles": ["Ecosif.Api.Access"],
"sub": "33333333-3333-4333-8333-333333333333",
"tid": "11111111-1111-4111-8111-111111111111",
"ver": "2.0"
}
Ordem de lookup B2B (implementada AUTH-14.3)¶
oiddo service principalappidazp
Provider: AZURE_ENTERPRISE_APP (ou ECOSIF_AZURE_APP_LINK_PROVIDER).
Detecção app-only vs humano (implementada)¶
| Token | idtyp |
scp |
roles |
Classificação |
|---|---|---|---|---|
| App-only | app |
ausente | presente | App-only |
| App-only (variante) | ausente | ausente | presente | App-only |
| Humano delegated | user ou ausente |
presente | opcional | Humano — ramo AZURE_ENTERPRISE |
Testes reprodutíveis: AzureEntraAppOnlySupportTest, AzureEntraAppOnlySpikeTest (starter).
4. Cenários pós-AUTH-14.3 (backend)¶
| Config | Vínculo BD | Token | HTTP backend |
|---|---|---|---|
ECOSIF_ENABLE_AZURE_APP_ONLY=false |
— | app-only válido | 401 (app_only_disabled no log) |
true |
ausente | app-only válido | 401 (no_identity_provider_link) |
true |
presente | role OK | 200 (se ACL endpoint OK) |
true |
presente | role ausente | 401 (BadJwtException) |
true |
presente | appid não allowlisted |
401 (BadJwtException) |
5. Pendências do cliente (fechar na homolog AUTH-14.9)¶
- [ ] Confirmar se 403 vem do gateway (corpo + headers
x-ms-*/ APIM trace) - [ ] Enviar payload jwt.ms (sem secret) do token real
- [ ] Confirmar App role name = valor em
ECOSIF_AZURE_APP_REQUIRED_ROLES - [ ] Confirmar
oiddo service principal para onboarding (POST /api/admin/b2b-app-links)
6. Referências¶
| Artefato | Caminho |
|---|---|
| Plano AUTH-14 | hub .internal_docs/tasks-plan/in-progress/20260825_AUTH-14_entra-app-only-b2b.md |
| Integração B2B | integracao_b2b_entra_app_only.md |
| Checklist ops | checklist_b2b_entra_app_only.md |
| Starter detector | AzureEntraAppOnlySupport.java |
| Logs M2M | AzureEntraAppOnlyAuthAudit.java |