Configurações e Modos de Integração
Nesta modalidade de autenticação, a interface completa da aplicação é disponibilizada ao cliente. Qualquer restrição visual ou de regra de negócio ocorre em conformidade com as configurações predefinidas para os diferentes perfis de usuários. O fluxo operacional assemelha-se ao cenário em que o próprio usuário insere a URL da aplicação no navegador e utiliza o formulário de login convencional.
Contrato de Integração de Relatórios do ReportServer
Objetivo
Este documento descreve o contrato de integração determinístico usado por sistemas externos para autenticar um usuário mapeado e abrir relatórios no ReportServer.
Não confundir: ExternalAuth x ReportIntegration
Este contrato cobre apenas o fluxo app-app de bootstrap de integração.
- ReportIntegration (este documento): autenticação entre sistemas via API key + token temporário de integração.
- ExternalAuth: autenticação de credenciais do usuário (username/senha) no login direto do ReportServer.
Importante:
- No endpoint /api/integration/bootstrap-report-session, não há validação de senha do usuário.
- A validação do bootstrap usa ReportIntegration:RequestApiKey* e BootstrapTokenSigningKey.
Parte 1 — Sistema Integrador (Lado do Cliente)
Nesta parte ficam os fluxos que o cliente precisa implementar no próprio servidor (chamadas, modos Full/List/OneReport e abertura da launchUrl).
Modos de Integração
- Full: abre o ServerApp completo (comportamento padrão atual).
- List: usa POST /api/integration/list-reports para retornar os relatórios acessíveis (com ou sem filtro por reportNames) e, em seguida, abre o fluxo padrão (mode = Full) no navegador.
- OneReport: usa mode = EmbeddedOneReport no bootstrap para abrir a página restrita de um único relatório por reportId.
Diferença prática entre List e OneReport
- OneReport é estritamente por ID?(reportId) e exige regras extras do modo embarcado.
- List é por nome?(reportNames) e pode retornar 1..N itens.
- Se List receber apenas um nome e retornar um único item, o efeito visual pode parecer "um relatório só", mas tecnicamente continua sendo fluxo de lista (não é EmbeddedOneReport).
Resumo do Fluxo
- O sistema cliente envia uma requisição de bootstrap server-to-server.
- O ReportServer valida a chave de integração compartilhada e o payload da requisição.
- O ReportServer gera um token assinado, de curta duração e uso único.
- O ReportServer retorna uma URL de abertura contendo o token.
- O navegador abre /integration/entry com token e returnUrl opcional.
- IntegrationEntry consome o token, autentica o usuário mapeado e redireciona conforme o modo do token.
- O usuário continua usando os recursos padrão de relatório, já protegidos por papéis e permissões do ReportServer.
Cenários de Integração Validados (Atuais)
- Listar relatórios por filtro (server-to-server)
- Chamar POST /api/integration/list-reports com reportNames.
- O resultado retorna apenas relatórios acessíveis e compatíveis com os nomes solicitados.
- Abrir ServerApp completo após listagem
- Chamar POST /api/integration/bootstrap-report-session com mode = Full.
- Abrir a launchUrl retornada no navegador.
- Abrir um relatório por ID
- Chamar POST /api/integration/bootstrap-report-session com mode = EmbeddedOneReport e reportId positivo.
- Chave de integração inválida
- Requisição negada (401 invalid-integration-key).
- Usuário externo desconhecido
- Requisição negada (404 external-user-not-found).
- Falhas de validação de payload
- Campos obrigatórios ausentes/inválidos retornam 400 com código determinístico.
- Falhas de validação de token
- Token inválido/expirado/reutilizado é negado em /integration/entry.
- Nomes não encontrados ou sem permissão no list-reports
- Retornados em notFoundOrNotAllowed.
- Login direto no ServerApp vs contexto de integração externa
- O filtro automático por reportNames só deve ser aplicado no fluxo de integração.
- Login direto não deve aplicar automaticamente filtro de integração.
- Segurança na troca de sessão (usuário de integração → usuário de login manual)
- O login força isolamento de reautenticação para evitar crossover de antiforgery/sessão antiga.
Regras extras do EmbeddedOneReport
- A requisição deve incluir mode = EmbeddedOneReport e um reportId positivo válido.
- O relatório alvo deve estar marcado como global (AhcReport.IsGlobal = true).
- O relatório global continua respeitando as regras de acesso/distribuição do ServerApp (owner, RestrictAccess, perfis/claims).
- Se reportId estiver ausente/inválido ou o relatório não for global, o acesso é negado.
Endpoint de Bootstrap
- Método:?POST
- Rota:?/api/integration/bootstrap-report-session
- Content-Type:?application/json
Header obrigatório
- O nome do header é configurável por ReportIntegration:RequestApiKeyHeaderName.
- Nome de header padrão configurado: X-Report-Integration-Key.
- O valor do header deve corresponder a ReportIntegration:RequestApiKey.
- Essa chave é um segredo compartilhado entre o AdhocReport e o servidor de autorização do cliente.
Corpo da Requisição
{
"externalUserId": "external-user-id",
"clientSystemId": "client-system-id",
"returnUrl": "/optional-relative-return",
"reportId": 123,
"mode": "EmbeddedOneReport"
}
Endpoint de Listagem de Relatórios (Server-to-Server)
- Método:?POST
- Rota:?/api/integration/list-reports
- Content-Type:?application/json
Header obrigatório
- Mesmo header do bootstrap:
- Nome por ReportIntegration:RequestApiKeyHeaderName
- Valor por ReportIntegration:RequestApiKey
Corpo da Requisição
{
"externalUserId": "ext-usr-001",
"clientSystemId": "client-system-demo",
"reportNames": ["Vendas Mensais", "Fluxo de Caixa"],
"matchMode": 0,
"includeOnlyExecutable": true
}
Regras
- externalUserId e clientSystemId são obrigatórios.
- reportNames é opcional:
- vazio/ausente ⇒ retorna todos os relatórios acessíveis para o usuário externo.
- informado ⇒ filtra por nomes (comparação Exact, case-insensitive).
- Limite de reportNames: 50 itens.
- matchMode suportado atualmente: Exact.
- Fail-closed: nomes não encontrados ou sem permissão voltam em notFoundOrNotAllowed.
Resposta de Sucesso
HTTP 200 OK
{
"success": true,
"message": "ok",
"reference": "trace-or-correlation-id",
"requested": 2,
"returned": 1,
"notFoundOrNotAllowed": ["Fluxo de Caixa"],
"items": [
{
"id": 123,
"name": "Vendas Mensais",
"description": "Resumo por mês",
"isGlobal": true
}
]
}
Códigos de erro esperados
- 400: invalid-request, invalid-list-reports-payload, too-many-report-names, unsupported-match-mode
- 401: invalid-integration-key
- 403: report-integration-disabled
- 404: external-user-not-found
- 500: report-integration-misconfigured
Exemplo cURL (listagem)
curl -X POST "https://localhost:7019/api/integration/list-reports" \
-H "Content-Type: application/json" \
-H "X-Report-Integration-Key: dev-report-integration-key" \
-d '{"externalUserId":"ext-usr-001","clientSystemId":"client-system-demo","reportNames":["Vendas Mensais"]}'
Exemplo cURL (todos os relatórios acessíveis do usuário)
curl -X POST "https://localhost:7019/api/integration/list-reports" \
-H "Content-Type: application/json" \
-H "X-Report-Integration-Key: dev-report-integration-key" \
-d '{"externalUserId":"ext-usr-001","clientSystemId":"client-system-demo"}'
Comportamento:
- Quando reportNames não é enviado ou vem vazio, o ServerApp retorna todos os relatórios acessíveis para esse usuário externo.
Regras dos campos
- externalUserId: obrigatório, deve mapear para um usuário existente no ReportServer (ApplicationUser.ExternalUserId).
- clientSystemId: obrigatório, identifica o sistema chamador.
- returnUrl: opcional, quando informado deve ser um caminho relativo iniciando com /.
- mode: opcional. Valores permitidos: Full (padrão) ou EmbeddedOneReport.
- Compatibilidade de serialização de enum:
- recomenda-se enviar valores numéricos no payload:
- mode: 0 = Full, 1 = EmbeddedOneReport
- matchMode: 0 = Exact
- reportId: obrigatório quando mode = EmbeddedOneReport; deve ser inteiro positivo.
Resposta de Sucesso
HTTP 200 OK
{
"success": true,
"message": "ok",
"launchUrl": "https://server/integration/entry?token=...",
"expiresAtUtc": "2026-06-02T18:30:00Z",
"reference": "trace-or-correlation-id"
}
Respostas de Erro
- 400 BadRequest
- invalid-request
- invalid-bootstrap-payload
- invalid-return-url
- 401 Unauthorized
- invalid-integration-key
- 403 Forbidden
- report-integration-disabled
- 404 NotFound
- external-user-not-found
- 500 InternalServerError
- report-integration-misconfigured
Todos os payloads de erro seguem:
{
"success": false,
"message": "error-code",
"launchUrl": null,
"expiresAtUtc": null,
"reference": "trace-or-correlation-id"
}
Integração Mínima do Cliente (Guia Enxuto)
O cliente externo só precisa de duas ações:
- Chamar o endpoint de bootstrap (server-to-server).
- Abrir a launchUrl retornada no navegador.
O ReportIntegration.BlazorTestClient é um app de diagnóstico e não é obrigatório para integração de produção do cliente.
Exemplo cURL
curl -X POST "https://localhost:7019/api/integration/bootstrap-report-session" \
-H "Content-Type: application/json" \
-H "X-Report-Integration-Key: dev-report-integration-key" \
-d '{"externalUserId":"ext-usr-001","clientSystemId":"client-system-demo","returnUrl":"/"}'
Exemplo JavaScript
const response = await fetch("https://localhost:7019/api/integration/bootstrap-report-session", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Report-Integration-Key": "dev-report-integration-key"
},
body: JSON.stringify({
externalUserId: "ext-usr-001",
clientSystemId: "client-system-demo",
returnUrl: "/",
mode: 1,
reportId: 123
})
});
const payload = await response.json();
if (payload.success && payload.launchUrl) {
window.open(payload.launchUrl, "_blank");
}
Exemplo C#
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-Report-Integration-Key", "dev-report-integration-key");
var body = new
{
externalUserId = "ext-usr-001",
clientSystemId = "client-system-demo",
returnUrl = "/"
};
var response = await client.PostAsJsonAsync("https://localhost:7019/api/integration/bootstrap-report-session", body);
var payload = await response.Content.ReadFromJsonAsync<BootstrapResponse>();
if (payload?.Success == true && !string.IsNullOrWhiteSpace(payload.LaunchUrl))
{
// Abra payload.LaunchUrl no navegador no fluxo da aplicação hospedeira.
}
public sealed record BootstrapResponse(bool Success, string? Message, string? LaunchUrl, DateTimeOffset? ExpiresAtUtc, string? Reference);
Parte 2 — ReportServer (Lado do Servidor)
Nesta parte ficam as configurações e garantias operacionais que o ambiente do ReportServer precisa cumprir para atender a integração.
Comportamento do Token
- O payload do token contém:
- externalUserId
- clientSystemId
- mode
- reportId
- tokenId
- issuedAtUtc
- expiresAtUtc
- Algoritmo de assinatura: HMACSHA256 com ReportIntegration:BootstrapTokenSigningKey.
- O token é de curta duração?(BootstrapTokenLifetimeSeconds).
- O token é de uso único?(replay é bloqueado com cache em memória).
- Falhas de validação incluem:
- missing-token
- invalid-token-format
- invalid-token-signature
- invalid-token-payload
- expired-token
- token-already-used
- report-integration-misconfigured
Configuração Obrigatória
Em appsettings / ambiente:
- ReportIntegration:Enabled
- ReportIntegration:RequestApiKeyHeaderName
- ReportIntegration:RequestApiKey
- ReportIntegration:BootstrapTokenLifetimeSeconds
- ReportIntegration:BootstrapTokenSigningKey
- ReportIntegration:IntegrationReportsRoute
Configuração típica (desenvolvimento):
"ReportIntegration": {
"Enabled": true,
"RequestApiKeyHeaderName": "X-Report-Integration-Key",
"RequestApiKey": "dev-report-integration-key",
"BootstrapTokenLifetimeSeconds": 120,
"BootstrapTokenSigningKey": "dev-bootstrap-signing-key-change-in-production",
"IntegrationReportsRoute": "/integration/entry",
"Cors": {
"AllowedOrigins": [
"https://localhost:7227",
"http://localhost:5148"
]
}
}
Observações:
- BootstrapTokenSigningKey é lida de configuração. Se ausente, o sistema falha com report-integration-misconfigured.
- Cors.AllowedOrigins é necessário quando o cliente chama o bootstrap em contexto cross-origin.
Notas de Segurança
- Não exponha a chave compartilhada nem a chave de assinatura em código client-side.
- Mantenha BootstrapTokenSigningKey obrigatório; não use valores de fallback hardcoded.
- Não trate IsGlobal como bypass de autorização: a elegibilidade global e a autorização do usuário são verificações distintas e cumulativas.
- Mantenha todas as chamadas em HTTPS.
- Use o reference da resposta para correlacionar erros visíveis ao usuário com logs do servidor.
Observabilidade em Runtime
Eventos de segurança e auditoria são registrados para:
- Bootstrap negado e motivo.
- Bootstrap bem-sucedido e contexto da URL gerada.
- Negação/abertura de sessão na rota de integração.