Manual do Usuário de Autenticação Externa (ReportServer)

1. Objetivo

Este manual explica como configurar e operar autenticação externa no ReportServer usando o modelo atual de webhook direto.

Ele é destinado a administradores, times de suporte e operadores técnicos que precisam gerenciar usuários externos e diagnosticar problemas de login.


2. Visão Geral do Modelo de Autenticação

O ReportServer suporta usuários locais e usuários externos.

Usuários locais

  • Armazenados e validados pelo ASP.NET Identity (AspNetUsers).
  • A validação de senha é feita localmente.

Usuários externos

  • Validados por um endpoint webhook fornecido pelo cliente.
  • A requisição de autenticação é enviada do ReportServer diretamente ao serviço externo.
  • Após o primeiro login externo bem-sucedido, um registro local de identidade é provisionado e vinculado.

3. Configuração Obrigatória

Seção de configuração em appsettings*.json:

"ExternalAuth": {
  "Enabled": true,
  "EndpointUrl": "https://localhost:5199/api/external-auth/validate",
  "TimeoutSeconds": 10,
  "IgnoreTlsErrorsForDevelopment": true
}

Significado das chaves

  • Enabled: habilita/desabilita o fluxo de autenticação externa.
  • EndpointUrl: endpoint webhook HTTPS completo usado para validar credenciais.
  • TimeoutSeconds: tempo máximo de espera pela resposta do webhook.
  • IgnoreTlsErrorsForDevelopment: flag somente de desenvolvimento para aceitar certificados locais/self-signed em testes localhost.

Precedência de ambiente

appsettings.Development.json sobrescreve appsettings.json quando o ambiente é Development.


4. Contrato do Webhook

O ReportServer envia JSON com:

  • password (sempre)
  • username (quando disponível)
  • externalUserId (quando usuário já está vinculado)

Nenhum campo técnico adicional é obrigatório (por exemplo: sem CorrelationId e sem metadados de provisionamento de role), para manter a implementação do endpoint do cliente simples.

Formato de sucesso aceito:

{
  "isAuthenticated": true,
  "userContext": {
       "userId": "ext-usr-001"
  },
  "fullName": "External User",
  "email": "external.user@domain.com"
}

Campos obrigatórios em autenticação bem-sucedida

Quando isAuthenticated=true, a resposta deve incluir:

  • userContext.userId
  • email

Se email estiver ausente ou vazio, a autenticação é negada pelo ReportServer.

Se a autenticação falhar, o webhook deve retornar payload explícito de negação (HTTP 200 com isAuthenticated=false) ou status de erro.


5. Ciclo de Vida de Usuário Externo

Primeiro login externo bem-sucedido

  1. Usuário não é encontrado localmente.
  2. ReportServer chama o webhook externo.
  3. Se válido, ReportServer provisiona o registro local no Identity e vincula:
    • ExternalProvider
    • ExternalUserId

Logins externos subsequentes

  1. Usuário local é encontrado como externo-vinculado.
  2. ReportServer valida externamente novamente (usando externalUserId quando disponível).
  3. ReportServer compara o email retornado com o email local do Identity.
  4. Se mudou, ReportServer atualiza o Identity com o novo email.
  5. Usuário entra no sistema se a validação for bem-sucedida.

Por que email é obrigatório

Email é essencial para comunicação do ciclo de vida dos relatórios.

  • Notificações de expiração são enviadas por email para report managers e/ou donos dos relatórios.
  • Notificações de quarentena e pós-quarentena também dependem de email válido.
  • Sem email válido, usuários podem não receber alertas antes da remoção automática de relatórios.

Se o cliente fornecer email inválido, esse impacto operacional é responsabilidade do cliente, mas o campo deve sempre estar presente.


6. Checklist Manual de Validação

Use antes da liberação em produção:

  1. Credenciais externas válidas -> login bem-sucedido.
  2. Senha inválida -> login negado com mensagem segura.
  3. Usuário desconhecido -> login negado.
  4. Webhook indisponível -> falha controlada (timeout/rede).
  5. Repetir login com o mesmo usuário externo -> fluxo de usuário vinculado funciona.
  6. ExternalAuth.Enabled=false -> usuários externos devem ser negados.
  7. Logs contêm eventos de rota e sucesso/falha sem dados sensíveis.

7. Troubleshooting

Sintoma: login externo sempre negado

Verificar:

  1. ExternalAuth.Enabled no arquivo de ambiente ativo.
  2. Correção e alcance de EndpointUrl.
  3. Formato da resposta do webhook (isAuthenticated + userContext.userId).
  4. Logs do servidor para eventos ExternalAuthWebhook*.

Sintoma: falha de handshake SSL/TLS

Detalhes típicos em log:

  • The SSL connection could not be established
  • UntrustedRoot

Ações:

  1. Em cenário localhost de desenvolvimento, usar IgnoreTlsErrorsForDevelopment=true.
  2. Garantir endpoint HTTPS em execução.
  3. Fora de ambiente local, usar certificados confiáveis e manter validação TLS estrita.

Sintoma: comportamento não bate com appsettings.json

Causa:

  • appsettings.Development.json sobrescreve a configuração base em Development.

Ação:

  • Verificar valores efetivos no arquivo ativo do ambiente e reiniciar a aplicação.

8. Recomendações de Segurança

  1. Manter IgnoreTlsErrorsForDevelopment=false fora de homologação local.
  2. Usar certificados confiáveis em ambientes compartilhados/produção.
  3. Manter timeout curto do webhook e monitorar falhas.
  4. Evitar expor detalhes sensíveis de exceção para usuário final.
  5. Usar logs para diagnóstico com referências de correlação.

9. Runbook Operacional (Rápido)

  1. Subir serviço externo de autenticação.
  2. Verificar endpoint /health.
  3. Confirmar configurações ExternalAuth.
  4. Subir ReportServer.
  5. Rodar checklist manual de login.
  6. Revisar logs e registrar evidências.

10. Template de Evidência para Suporte

Para cada incidente, capturar:

  • Timestamp
  • Username testado
  • Ambiente
  • Evento de log da rota selecionada
  • Linha de log de erro/sucesso do webhook
  • Resultado final visível para usuário

Essa evidência é suficiente para triagem de primeiro nível e escalonamento para engenharia.


Exemplos de Servidor de Autenticação Externa (para login direto no ReportServer)

Objetivo

Este documento fornece implementações de referência mínimas para o servidor de autenticação externa do cliente usado no fluxo de login direto via URL do ReportServer.

Use estes exemplos quando o usuário abre a URL do ReportServer, informa credenciais na tela de login e o ReportServer chama seu endpoint externo para validar as credenciais.


Contrato esperado pelo ReportServer

Requisição recebida do ReportServer (POST JSON)

{
  "username": "optional-username",
  "password": "required-password",
  "externalUserId": "optional-external-id"
}

Regras observadas no código atual:

  • password é sempre enviado.
  • username pode ser enviado.
  • externalUserId pode ser enviado (usuários vinculados).
  • quando externalUserId é enviado, ele deve ter prioridade na busca em relação ao username.

Resposta de sucesso (mínimo)

{
  "isAuthenticated": true,
  "userContext": {
       "userId": "ext-usr-001"
  },
  "fullName": "External User",
  "email": "external.user@domain.com"
}

Resposta de negação

{
  "isAuthenticated": false,
  "message": "invalid-credentials"
}

Resposta de requisição inválida

{
  "isAuthenticated": false,
  "message": "invalid-request"
}

Importante:

  • Em sucesso, retorne email válido (obrigatório pelo ReportServer).
  • userContext.userId é o formato preferido.
  • externalUserId na raiz também é aceito como fallback.

Exemplo 1 - Node.js (Express)

import express from "express";
import bcrypt from "bcryptjs";

const app = express();
app.use(express.json());

// Substituir por integração com store seguro / provedor de identidade real.
const users = [
  {
       username: "external.user",
       externalUserId: "ext-usr-001",
       isActive: true,
       passwordHash: bcrypt.hashSync("external.pass", 10),
       fullName: "External User",
       email: "external.user@domain.com"
  }
];

app.get("/health", (_req, res) => {
  return res.status(200).json({
       status: "ok",
       source: "ExternalAuth.SampleNode"
  });
});

app.post("/api/external-auth/validate", (req, res) => {
  const { username, password, externalUserId } = req.body ?? {};

       if ((!username && !externalUserId) || !password) {
       return res.status(400).json({
         isAuthenticated: false,
         message: "invalid-request"
       });
  }

  const user = users.find(u =>
       (externalUserId && u.externalUserId === externalUserId) ||
       (username && u.username === username)
  );

       if (!user || !user.isActive) {
       return res.status(200).json({
         isAuthenticated: false,
         message: "invalid-credentials"
       });
  }

  const passwordMatches = bcrypt.compareSync(password, user.passwordHash);
  if (!passwordMatches) {
       return res.status(200).json({
         isAuthenticated: false,
         message: "invalid-credentials"
       });
  }

  return res.status(200).json({
       isAuthenticated: true,
       userContext: { userId: user.externalUserId },
       fullName: user.fullName,
       email: user.email
  });
});

app.listen(5199, () => {
  console.log("Servidor de auth externa ouvindo em https://localhost:5199");
  console.log("Health: GET /health");
  console.log("Validate: POST /api/external-auth/validate");
});


Exemplo 2 - C# (.NET Minimal API)

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var passwordHasher = new PasswordHasher<ExternalUser>();

var externalUser = new ExternalUser(
       Username: "external.user",
       ExternalUserId: "ext-usr-001",
       PasswordHash: string.Empty,
       FullName: "External User",
       Email: "external.user@domain.com",
       IsActive: true);

var externalUserWithHash = externalUser with
{
       PasswordHash = passwordHasher.HashPassword(externalUser, "external.pass")
};

var users = new[]
{
       externalUserWithHash
};

app.MapGet("/health", () => Results.Ok(new { status = "ok", source = "ExternalAuth.SampleDotNet" }));

app.MapPost("/api/external-auth/validate", (ExternalAuthRequest request) =>
{
       if ((string.IsNullOrWhiteSpace(request.Username) && string.IsNullOrWhiteSpace(request.ExternalUserId))
               || string.IsNullOrWhiteSpace(request.Password))
       {
               return Results.BadRequest(new
               {
                       isAuthenticated = false,
                       message = "invalid-request"
               });
       }

       var user = users.FirstOrDefault(u =>
               (!string.IsNullOrWhiteSpace(request.ExternalUserId) && u.ExternalUserId == request.ExternalUserId) ||
               (!string.IsNullOrWhiteSpace(request.Username) && u.Username == request.Username));

       if (user is null || !user.IsActive)
       {
               return Results.Ok(new
               {
                       isAuthenticated = false,
                       message = "invalid-credentials"
               });
       }

       var verifyResult = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, request.Password);
       if (verifyResult == PasswordVerificationResult.Failed)
       {
               return Results.Ok(new
               {
                       isAuthenticated = false,
                       message = "invalid-credentials"
               });
       }

       return Results.Ok(new
       {
               isAuthenticated = true,
               userContext = new { userId = user.ExternalUserId },
               fullName = user.FullName,
               email = user.Email
       });
});

app.Run("https://localhost:5199");

public sealed record ExternalAuthRequest(string? Username, string Password, string? ExternalUserId);
public sealed record ExternalUser(string Username, string ExternalUserId, string PasswordHash, string FullName, string Email, bool IsActive);


Notas de produção

  • Mantenha este endpoint somente em HTTPS.
  • Nunca registre senha em texto plano nos logs.
  • Use backend real de identidade (LDAP/AD/banco/provedor), não usuários em memória.
  • Mantenha mensagens de erro genéricas para evitar enumeração de credenciais.
  • Adicione timeout, rate limit e logs de auditoria.

Cenários manuais de teste

  1. Credenciais válidas (username + password) -> isAuthenticated=true.
  2. Usuário vinculado válido (externalUserId + password) -> isAuthenticated=true.
  3. Senha inválida -> isAuthenticated=false, message=invalid-credentials.
  4. Usuário desconhecido -> isAuthenticated=false, message=invalid-credentials.
  5. Campos ausentes -> HTTP 400 com message=invalid-request.
  6. Endpoint indisponível -> ReportServer registra tratamento de timeout/rede.