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

  1. O sistema cliente envia uma requisição de bootstrap server-to-server.
  2. O ReportServer valida a chave de integração compartilhada e o payload da requisição.
  3. O ReportServer gera um token assinado, de curta duração e uso único.
  4. O ReportServer retorna uma URL de abertura contendo o token.
  5. O navegador abre /integration/entry com token e returnUrl opcional.
  6. IntegrationEntry consome o token, autentica o usuário mapeado e redireciona conforme o modo do token.
  7. 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)

  1. 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.
  2. Abrir ServerApp completo após listagem
    • Chamar POST /api/integration/bootstrap-report-session com mode = Full.
    • Abrir a launchUrl retornada no navegador.
  3. Abrir um relatório por ID
    • Chamar POST /api/integration/bootstrap-report-session com mode = EmbeddedOneReport e reportId positivo.
  4. Chave de integração inválida
    • Requisição negada (401 invalid-integration-key).
  5. Usuário externo desconhecido
    • Requisição negada (404 external-user-not-found).
  6. Falhas de validação de payload
    • Campos obrigatórios ausentes/inválidos retornam 400 com código determinístico.
  7. Falhas de validação de token
    • Token inválido/expirado/reutilizado é negado em /integration/entry.
  8. Nomes não encontrados ou sem permissão no list-reports
    • Retornados em notFoundOrNotAllowed.
  9. 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.
  10. 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:

  1. Chamar o endpoint de bootstrap (server-to-server).
  2. 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.