Autenticação Direta Via Portal
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
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
- Usuário não é encontrado localmente.
- ReportServer chama o webhook externo.
- Se válido, ReportServer provisiona o registro local no Identity e vincula:
- ExternalProvider
- ExternalUserId
Logins externos subsequentes
- Usuário local é encontrado como externo-vinculado.
- ReportServer valida externamente novamente (usando externalUserId quando disponível).
- ReportServer compara o email retornado com o email local do Identity.
- Se mudou, ReportServer atualiza o Identity com o novo email.
- 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:
- Credenciais externas válidas -> login bem-sucedido.
- Senha inválida -> login negado com mensagem segura.
- Usuário desconhecido -> login negado.
- Webhook indisponível -> falha controlada (timeout/rede).
- Repetir login com o mesmo usuário externo -> fluxo de usuário vinculado funciona.
- ExternalAuth.Enabled=false -> usuários externos devem ser negados.
- Logs contêm eventos de rota e sucesso/falha sem dados sensíveis.
7. Troubleshooting
Sintoma: login externo sempre negado
Verificar:
- ExternalAuth.Enabled no arquivo de ambiente ativo.
- Correção e alcance de EndpointUrl.
- Formato da resposta do webhook (isAuthenticated + userContext.userId).
- 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:
- Em cenário localhost de desenvolvimento, usar IgnoreTlsErrorsForDevelopment=true.
- Garantir endpoint HTTPS em execução.
- 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
- Manter IgnoreTlsErrorsForDevelopment=false fora de homologação local.
- Usar certificados confiáveis em ambientes compartilhados/produção.
- Manter timeout curto do webhook e monitorar falhas.
- Evitar expor detalhes sensíveis de exceção para usuário final.
- Usar logs para diagnóstico com referências de correlação.
9. Runbook Operacional (Rápido)
- Subir serviço externo de autenticação.
- Verificar endpoint /health.
- Confirmar configurações ExternalAuth.
- Subir ReportServer.
- Rodar checklist manual de login.
- 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
- Credenciais válidas (username + password) -> isAuthenticated=true.
- Usuário vinculado válido (externalUserId + password) -> isAuthenticated=true.
- Senha inválida -> isAuthenticated=false, message=invalid-credentials.
- Usuário desconhecido -> isAuthenticated=false, message=invalid-credentials.
- Campos ausentes -> HTTP 400 com message=invalid-request.
- Endpoint indisponível -> ReportServer registra tratamento de timeout/rede.