Plano de Extração do BFF para Aplicação Externa com Autenticação Própria

Data: 2026-01-28
Status: 📋 Plano Detalhado
Objetivo: Extrair BFF de módulo interno para aplicação externa com autenticação própria e suporte a múltiplos apps consumidores (OAuth2 Client Credentials)


Extrair o BFF (Backend for Frontend) de módulo interno para uma aplicação ASP.NET Core separada que:

  1. Tem autenticação própria (OAuth2 Client Credentials Flow)
  2. Registra múltiplos apps consumidores (BFF, Mobile App, Web App, etc.)
  3. Consome a API principal via HTTP com autenticação
  4. Escala independentemente da API principal
  5. Mantém compatibilidade com API v1 existente

📊 Arquitetura Proposta

┌─────────────────────────────────────────────────────────┐
│              Aplicações Cliente (Frontend)               │
│  - Flutter App (Mobile)                                 │
│  - Web App (React/Vue)                                  │
│  - Admin Dashboard                                       │
└────────────────────┬────────────────────────────────────┘
                     │
                     │ HTTP/REST + OAuth2 Bearer Token
                     │ (Client Credentials Flow)
                     │
┌────────────────────▼────────────────────────────────────┐
│         Arah.Api.Bff (Aplicação Externa)            │
│  ┌────────────────────────────────────────────────────┐  │
│  │  OAuth2 Authorization Server                        │  │
│  │  - Client Registration                              │  │
│  │  - Token Endpoint (/oauth/token)                   │  │
│  │  - Client Credentials Flow                          │  │
│  └────────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Journey Controllers                               │  │
│  │  - FeedJourneyController                          │  │
│  │  - EventJourneyController                         │  │
│  │  - MarketplaceJourneyController                    │  │
│  └────────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Journey Services (orquestração)                    │  │
│  │  - FeedJourneyService                             │  │
│  │  - EventJourneyService                            │  │
│  └────────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────────┐  │
│  │  API Client (consome API principal)                │  │
│  │  - ApiHttpClient (com retry, circuit breaker)      │  │
│  │  - Token forwarding (BFF token → API token)       │  │
│  └────────────────────────────────────────────────────┘  │
└────────────────────┬────────────────────────────────────┘
                     │
                     │ HTTP/REST + JWT Token
                     │ (Token do usuário repassado)
                     │
┌────────────────────▼────────────────────────────────────┐
│         Arah.Api (API Principal)                    │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Controllers (por recurso)                        │  │
│  │  - FeedController                                  │  │
│  │  - EventsController                                │  │
│  │  - MarketplaceController                           │  │
│  └────────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Application Services (lógica de negócio)          │  │
│  │  - FeedService                                     │  │
│  │  - EventsService                                  │  │
│  └────────────────────────────────────────────────────┘  │
└───────────────────────────────────────────────────────────┘

Fluxo de Autenticação

Admin → POST /api/v1/admin/clients
{
  "name": "Flutter Mobile App",
  "description": "Aplicativo mobile Flutter",
  "scopes": ["journeys:read", "journeys:write"],
  "redirectUris": ["Arah://callback"]
}

Response:
{
  "clientId": "550e8400-e29b-41d4-a716-446655440000",
  "clientSecret": "super-secret-key-here",
  "createdAt": "2026-01-28T10:00:00Z"
}
App → POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=550e8400-e29b-41d4-a716-446655440000
&client_secret=super-secret-key-here
&scope=journeys:read journeys:write

Response:
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "journeys:read journeys:write"
}
App → GET /api/v2/journeys/feed/territory-feed?territoryId=...
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

BFF → GET http://api-principal/api/v1/feed?territoryId=...
Authorization: Bearer <token-do-usuario-repassado>
X-BFF-Client-Id: 550e8400-e29b-41d4-a716-446655440000

📋 Componentes Necessários

1. Sistema de Registro de Clientes OAuth2

// Arah.Domain.OAuth/ClientApplication.cs
public sealed class ClientApplication
{
    public Guid Id { get; private set; }
    public string Name { get; private set; }
    public string Description { get; private set; }
    public string ClientId { get; private set; }
    public string ClientSecretHash { get; private set; }
    public IReadOnlyList<string> Scopes { get; private set; }
    public IReadOnlyList<string> RedirectUris { get; private set; }
    public bool IsActive { get; private set; }
    public DateTime CreatedAtUtc { get; private set; }
    public DateTime? LastUsedAtUtc { get; private set; }
    public Guid CreatedByUserId { get; private set; }
}
// Arah.Application.Interfaces.OAuth/IClientApplicationRepository.cs
public interface IClientApplicationRepository
{
    Task<ClientApplication?> GetByClientIdAsync(string clientId, CancellationToken cancellationToken);
    Task<ClientApplication?> GetByIdAsync(Guid id, CancellationToken cancellationToken);
    Task<IReadOnlyList<ClientApplication>> ListAsync(CancellationToken cancellationToken);
    Task AddAsync(ClientApplication client, CancellationToken cancellationToken);
    Task UpdateAsync(ClientApplication client, CancellationToken cancellationToken);
    Task DeleteAsync(Guid id, CancellationToken cancellationToken);
}
// Arah.Application.Services.OAuth/ClientRegistrationService.cs
public sealed class ClientRegistrationService
{
    public async Task<Result<(string clientId, string clientSecret)>> RegisterClientAsync(
        string name,
        string description,
        IReadOnlyList<string> scopes,
        IReadOnlyList<string> redirectUris,
        Guid createdByUserId,
        CancellationToken cancellationToken)
    {
        // Gerar clientId e clientSecret
        var clientId = Guid.NewGuid().ToString("N");
        var clientSecret = GenerateSecureSecret();
        var secretHash = HashSecret(clientSecret);
        
        var client = new ClientApplication(
            Guid.NewGuid(),
            name,
            description,
            clientId,
            secretHash,
            scopes,
            redirectUris,
            true,
            DateTime.UtcNow,
            null,
            createdByUserId);
        
        await _repository.AddAsync(client, cancellationToken);
        await _unitOfWork.CommitAsync(cancellationToken);
        
        return Result<(string, string)>.Success((clientId, clientSecret));
    }
}

2. OAuth2 Authorization Server

2.1 Token Service para BFF

// Arah.Api.Bff/Security/IBffTokenService.cs
public interface IBffTokenService
{
    string IssueClientToken(string clientId, IReadOnlyList<string> scopes);
    (string? clientId, IReadOnlyList<string>? scopes) ParseClientToken(string token);
}
// Arah.Api.Bff/Controllers/OAuthController.cs
[ApiController]
[Route("oauth")]
public sealed class OAuthController : ControllerBase
{
    [HttpPost("token")]
    [Consumes("application/x-www-form-urlencoded")]
    public async Task<IActionResult> Token([FromForm] TokenRequest request)
    {
        if (request.GrantType != "client_credentials")
        {
            return BadRequest(new { error = "unsupported_grant_type" });
        }
        
        var client = await _clientRepository.GetByClientIdAsync(
            request.ClientId, 
            cancellationToken);
        
        if (client is null || !client.IsActive)
        {
            return Unauthorized(new { error = "invalid_client" });
        }
        
        if (!VerifyClientSecret(request.ClientSecret, client.ClientSecretHash))
        {
            return Unauthorized(new { error = "invalid_client" });
        }
        
        var token = _tokenService.IssueClientToken(client.ClientId, client.Scopes);
        
        return Ok(new
        {
            access_token = token,
            token_type = "Bearer",
            expires_in = 3600,
            scope = string.Join(" ", client.Scopes)
        });
    }
}
// Arah.Api.Bff/Middleware/BffAuthenticationMiddleware.cs
public sealed class BffAuthenticationMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        if (!context.Request.Headers.TryGetValue("Authorization", out var authHeader))
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsJsonAsync(new { error = "missing_authorization" });
            return;
        }
        
        var token = ExtractBearerToken(authHeader);
        var (clientId, scopes) = _tokenService.ParseClientToken(token);
        
        if (clientId is null)
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsJsonAsync(new { error = "invalid_token" });
            return;
        }
        
        // Adicionar clientId ao contexto
        context.Items["BffClientId"] = clientId;
        context.Items["BffScopes"] = scopes;
        
        await next(context);
    }
}
// Arah.Api.Bff/Clients/ApiHttpClient.cs
public sealed class ApiHttpClient
{
    private readonly HttpClient _httpClient;
    private readonly string _apiBaseUrl;
    private readonly ILogger<ApiHttpClient> _logger;
    
    public async Task<TResponse> GetAsync<TResponse>(
        string endpoint,
        string? userToken,
        CancellationToken cancellationToken)
    {
        var request = new HttpRequestMessage(HttpMethod.Get, $"{_apiBaseUrl}{endpoint}");
        
        // Repassar token do usuário se fornecido
        if (!string.IsNullOrWhiteSpace(userToken))
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", userToken);
        }
        
        // Adicionar identificação do BFF
        request.Headers.Add("X-BFF-Client-Id", _currentClientId);
        
        var response = await _httpClient.SendAsync(request, cancellationToken);
        response.EnsureSuccessStatusCode();
        
        return await response.Content.ReadFromJsonAsync<TResponse>(cancellationToken);
    }
}

5. Repasse de Token do Usuário

O BFF precisa repassar o token do usuário para a API principal. Duas abordagens:

Opção A: Token do Usuário no Header Customizado

App → BFF: Authorization: Bearer <bff-client-token>
         X-User-Token: <user-token>

BFF → API: Authorization: Bearer <user-token>
         X-BFF-Client-Id: <client-id>
App → BFF: Authorization: Bearer <bff-client-token>
         Body: { "userToken": "<user-token>", ... }

BFF → API: Authorization: Bearer <user-token>
         X-BFF-Client-Id: <client-id>

Recomendação: Opção A (header customizado) - mais simples e padrão.


🗄️ Estrutura de Banco de Dados

CREATE TABLE oauth_clients (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(200) NOT NULL,
    description TEXT,
    client_id VARCHAR(100) NOT NULL UNIQUE,
    client_secret_hash VARCHAR(255) NOT NULL,
    scopes TEXT[] NOT NULL,
    redirect_uris TEXT[] NOT NULL,
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at_utc TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    last_used_at_utc TIMESTAMP WITH TIME ZONE,
    created_by_user_id UUID NOT NULL REFERENCES users(id),
    
    CONSTRAINT oauth_clients_name_not_empty CHECK (LENGTH(TRIM(name)) > 0),
    CONSTRAINT oauth_clients_client_id_not_empty CHECK (LENGTH(TRIM(client_id)) > 0)
);

CREATE INDEX idx_oauth_clients_client_id ON oauth_clients(client_id);
CREATE INDEX idx_oauth_clients_created_by_user_id ON oauth_clients(created_by_user_id);
CREATE INDEX idx_oauth_clients_is_active ON oauth_clients(is_active);
// Arah.Infrastructure.Postgres/Migrations/XXXXXX_AddOAuthClients.cs
public partial class AddOAuthClients : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "oauth_clients",
            columns: table => new
            {
                Id = table.Column<Guid>(type: "uuid", nullable: false),
                Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
                Description = table.Column<string>(type: "text", nullable: true),
                ClientId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
                ClientSecretHash = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
                Scopes = table.Column<string[]>(type: "text[]", nullable: false),
                RedirectUris = table.Column<string[]>(type: "text[]", nullable: false),
                IsActive = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
                CreatedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
                LastUsedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
                CreatedByUserId = table.Column<Guid>(type: "uuid", nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_oauth_clients", x => x.Id);
                table.ForeignKey(
                    name: "FK_oauth_clients_users_CreatedByUserId",
                    column: x => x.CreatedByUserId,
                    principalTable: "users",
                    principalColumn: "Id",
                    onDelete: ReferentialAction.Cascade);
                table.UniqueConstraint("AK_oauth_clients_ClientId", x => x.ClientId);
            });

        migrationBuilder.CreateIndex(
            name: "IX_oauth_clients_ClientId",
            table: "oauth_clients",
            column: "ClientId",
            unique: true);

        migrationBuilder.CreateIndex(
            name: "IX_oauth_clients_CreatedByUserId",
            table: "oauth_clients",
            column: "CreatedByUserId");

        migrationBuilder.CreateIndex(
            name: "IX_oauth_clients_IsActive",
            table: "oauth_clients",
            column: "IsActive");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(name: "oauth_clients");
    }
}

backend/
├── Arah.Api.Bff/                    # Nova aplicação BFF
│   ├── Controllers/
│   │   ├── OAuthController.cs           # OAuth2 token endpoint
│   │   ├── Journeys/
│   │   │   ├── FeedJourneyController.cs
│   │   │   └── EventJourneyController.cs
│   │   └── Admin/
│   │       └── ClientRegistrationController.cs
│   ├── Services/
│   │   ├── Journeys/
│   │   │   ├── FeedJourneyService.cs
│   │   │   └── EventJourneyService.cs
│   │   └── OAuth/
│   │       └── ClientRegistrationService.cs
│   ├── Clients/
│   │   └── ApiHttpClient.cs             # HTTP client para API principal
│   ├── Security/
│   │   ├── IBffTokenService.cs
│   │   ├── BffTokenService.cs
│   │   └── BffAuthenticationMiddleware.cs
│   ├── Middleware/
│   │   └── BffAuthenticationMiddleware.cs
│   ├── Program.cs
│   └── Arah.Api.Bff.csproj
│
├── Arah.Domain.OAuth/               # Novo domínio OAuth
│   ├── ClientApplication.cs
│   └── OAuthScopes.cs
│
├── Arah.Application/                 # Atualizar
│   └── Services/
│       └── OAuth/
│           └── ClientRegistrationService.cs
│
└── Arah.Infrastructure/              # Atualizar
    └── Postgres/
        ├── Entities/
        │   └── OAuthClientRecord.cs
        ├── Repositories/
        │   └── PostgresOAuthClientRepository.cs
        └── Migrations/
            └── XXXXXX_AddOAuthClients.cs

🔧 Implementação Passo a Passo

Fase 1: Preparação (1 semana)

1.1 Criar Domínio OAuth (2 dias)

  • Criar projeto Arah.Domain.OAuth
  • Criar ClientApplication entity
  • Criar OAuthScopes (enum/constants)
  • Criar interfaces de repositório

1.2 Criar Infraestrutura OAuth (2 dias)

  • Criar OAuthClientRecord (Postgres entity)
  • Criar PostgresOAuthClientRepository
  • Criar migration AddOAuthClients
  • Aplicar migration

1.3 Criar Serviços OAuth (1 dia)

  • Criar ClientRegistrationService
  • Implementar geração de clientId e clientSecret
  • Implementar hash de clientSecret (BCrypt/Argon2)
  • Testes unitários

Fase 2: OAuth2 Authorization Server (1 semana)

2.1 Token Service BFF (2 dias)

  • Criar IBffTokenService
  • Implementar BffTokenService (JWT para clientes)
  • Configurar JWT options para BFF
  • Testes unitários

2.2 OAuth2 Token Endpoint (2 dias)

  • Criar OAuthController
  • Implementar POST /oauth/token (client credentials)
  • Validação de clientId/clientSecret
  • Validação de scopes
  • Testes de integração

2.3 Middleware de Autenticação (1 dia)

  • Criar BffAuthenticationMiddleware
  • Validar token do cliente
  • Adicionar clientId ao contexto
  • Testes de integração

Fase 3: API Client e Integração (1 semana)

3.1 API HTTP Client (2 dias)

  • Criar ApiHttpClient
  • Implementar retry policy (Polly)
  • Implementar circuit breaker
  • Repasse de token do usuário
  • Header X-BFF-Client-Id
  • Testes unitários

3.2 Journey Services (2 dias)

  • Mover FeedJourneyService para BFF
  • Mover EventJourneyService para BFF
  • Atualizar para usar ApiHttpClient
  • Testes de integração

3.3 Journey Controllers (1 dia)

  • Mover controllers de Arah.Api para Arah.Api.Bff
  • Atualizar rotas para /api/v2/journeys/*
  • Aplicar middleware de autenticação
  • Testes de integração

Fase 4: Admin e Registro de Clientes (1 semana)

  • Criar ClientRegistrationController
  • POST /api/v1/admin/clients (registrar)
  • GET /api/v1/admin/clients (listar)
  • GET /api/v1/admin/clients/{id} (obter)
  • PUT /api/v1/admin/clients/{id} (atualizar)
  • DELETE /api/v1/admin/clients/{id} (desativar)
  • Autorização (apenas SystemAdmin)

4.2 Documentação (1 dia)

  • Documentar OAuth2 flow
  • Documentar endpoints de registro
  • Exemplos de uso
  • Atualizar Swagger/OpenAPI

Fase 5: Deploy e Configuração (1 semana)

  • Configurar appsettings.json para BFF
  • Configurar connection string (compartilhado ou separado)
  • Configurar JWT options
  • Configurar API principal URL
  • Variáveis de ambiente
  • Configurar logging (Serilog, Seq)
  • Configurar métricas (Prometheus, OpenTelemetry)
  • Configurar health checks

5.2 Deploy (2 dias)

  • Dockerfile para BFF
  • docker-compose atualizado
  • Health checks
  • Logging e monitoring
  • Configuração de ambiente (dev, staging, prod)

5.3 Testes End-to-End (1 dia)

  • Testar fluxo completo
  • Testar múltiplos clientes
  • Testar revogação de cliente
  • Testar rate limiting

Fase 6: Documentação e Observabilidade (1 semana)

6.1 Atualização de Documentação (3 dias)

  • Documentar OAuth2 flow completo
  • Documentar endpoints de registro de clientes
  • Atualizar Swagger/OpenAPI
  • Documentar configuração e deploy
  • Guias de integração para desenvolvedores
  • Exemplos de código
  • Troubleshooting guide

6.2 Configuração de Logs e Monitoramento (2 dias)

  • Configurar Serilog no BFF
  • Configurar Seq (se aplicável)
  • Configurar Prometheus metrics
  • Configurar OpenTelemetry tracing
  • Dashboards e alertas
  • Logging estruturado para auditoria

⏱️ Estimativa de Esforço

Fase Descrição Duração Esforço (horas)
Fase 1 Preparação (Domínio, Infra, Serviços) 1 semana 40h
Fase 2 OAuth2 Authorization Server 1 semana 40h
Fase 3 API Client e Integração 1 semana 40h
Fase 4 Admin e Registro de Clientes 1 semana 40h
Fase 5 Deploy e Configuração 1 semana 40h
Fase 6 Documentação e Observabilidade 1 semana 40h
TOTAL 6 semanas 240h

Detalhamento por Tarefa

Fase 1: Preparação (40h)

  • Domínio OAuth: 8h
  • Infraestrutura OAuth: 16h
  • Serviços OAuth: 8h
  • Testes: 8h

Fase 2: OAuth2 Authorization Server (40h)

  • Token Service BFF: 8h
  • OAuth2 Token Endpoint: 16h
  • Middleware de Autenticação: 8h
  • Testes: 8h

Fase 3: API Client e Integração (40h)

  • API HTTP Client: 16h
  • Journey Services: 16h
  • Journey Controllers: 8h

Fase 4: Admin e Registro de Clientes (40h)

  • Admin Controller: 24h
  • Documentação: 8h
  • Testes: 8h

Fase 5: Deploy e Configuração (40h)

  • Configuração: 16h
  • Deploy: 16h
  • Testes End-to-End: 8h

Fase 6: Documentação e Observabilidade (40h)

  • Atualização de Documentação: 24h
  • Configuração de Logs e Monitoramento: 16h

🔐 Segurança

1. Armazenamento de Client Secret

  • Hash com BCrypt/Argon2 (nunca armazenar em texto plano)
  • Rotação de secrets (permitir regenerar secret)
  • Validação de força (mínimo 32 caracteres)

2. Validação de Token

  • Assinatura JWT (HS256 com secret forte)
  • Expiração (tokens expiram em 1 hora)
  • Validação de issuer/audience
  • Validação de scopes

3. Rate Limiting

  • Rate limit por clientId (prevenir abuso)
  • Rate limit por IP (prevenir ataques)
  • Throttling (circuit breaker)

4. Logging e Auditoria

  • Log de registros de clientes
  • Log de uso de tokens
  • Log de falhas de autenticação
  • Auditoria de mudanças

📊 Considerações de Performance

1. Cache

  • Cache de clientes (Redis/MemoryCache)
  • Cache de tokens (TTL curto)
  • Invalidação de cache (quando cliente desativado)

2. Connection Pooling

  • HTTP Client pooling (reutilizar conexões)
  • Connection timeout (configurável)
  • Retry policy (Polly)

3. Monitoring

  • Métricas de latência (BFF → API)
  • Métricas de erro (taxa de falha)
  • Métricas de uso (tokens emitidos, clientes ativos)

✅ Checklist de Implementação

Fase 1: Preparação

  • Criar projeto Arah.Domain.OAuth
  • Criar ClientApplication entity
  • Criar OAuthClientRecord (Postgres)
  • Criar PostgresOAuthClientRepository
  • Criar migration AddOAuthClients
  • Criar ClientRegistrationService
  • Testes unitários

Fase 2: OAuth2 Authorization Server

  • Criar IBffTokenService
  • Implementar BffTokenService
  • Criar OAuthController (POST /oauth/token)
  • Criar BffAuthenticationMiddleware
  • Testes de integração

Fase 3: API Client e Integração

  • Criar ApiHttpClient
  • Implementar retry/circuit breaker
  • Mover FeedJourneyService para BFF
  • Mover EventJourneyController para BFF
  • Testes de integração

Fase 4: Admin e Registro

  • Criar ClientRegistrationController
  • Implementar CRUD de clientes
  • Documentação OAuth2
  • Atualizar Swagger

Fase 5: Deploy

  • Configurar appsettings.json
  • Configurar logging (Serilog, Seq)
  • Configurar métricas (Prometheus)
  • Configurar OpenTelemetry
  • Criar Dockerfile
  • Atualizar docker-compose
  • Health checks
  • Testes End-to-End
  • Criar BFF_OAUTH2_GUIDE.md
  • Criar BFF_DEVELOPER_INTEGRATION_GUIDE.md
  • Criar BFF_API_REFERENCE.md
  • Atualizar BFF_API_CONTRACT.yaml
  • Criar BFF_DEPLOYMENT_GUIDE.md
  • Atualizar documentação principal
  • Configurar Serilog no BFF
  • Implementar logging estruturado
  • Configurar métricas Prometheus
  • Configurar OpenTelemetry tracing
  • Criar dashboards e alertas

📚 Documentação Adicional

Endpoints OAuth2

Request:

Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=<client-id>
&client_secret=<client-secret>
&scope=journeys:read journeys:write

Response:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "journeys:read journeys:write"
}

Endpoints Admin

Request:

{
  "name": "Flutter Mobile App",
  "description": "Aplicativo mobile Flutter",
  "scopes": ["journeys:read", "journeys:write"],
  "redirectUris": ["Arah://callback"]
}

Response:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "clientId": "550e8400e29b41d4a716446655440000",
  "clientSecret": "super-secret-key-here",
  "name": "Flutter Mobile App",
  "description": "Aplicativo mobile Flutter",
  "scopes": ["journeys:read", "journeys:write"],
  "redirectUris": ["Arah://callback"],
  "isActive": true,
  "createdAtUtc": "2026-01-28T10:00:00Z"
}

⚠️ IMPORTANTE: O clientSecret só é retornado uma vez no momento do registro. Guarde-o com segurança!


📚 Atualização de Documentação

Documentos a Criar/Atualizar

1. Documentação OAuth2

Arquivo: docs/BFF_OAUTH2_GUIDE.md

Conteúdo:

  • Visão geral do OAuth2 Client Credentials Flow
  • Como registrar um novo cliente
  • Como obter token de acesso
  • Como usar token no BFF
  • Exemplos de código (C#, JavaScript, Flutter)
  • Troubleshooting comum

2. Guia de Integração para Desenvolvedores

Arquivo: docs/BFF_DEVELOPER_INTEGRATION_GUIDE.md

Conteúdo:

  • Passo a passo para integrar app com BFF
  • Fluxo de autenticação completo
  • Exemplos de requisições
  • Tratamento de erros
  • Best practices
  • Rate limiting e quotas

3. Documentação de API BFF

Arquivo: docs/BFF_API_REFERENCE.md

Conteúdo:

  • Endpoints OAuth2 (/oauth/token)
  • Endpoints de Journeys (/api/v2/journeys/*)
  • Endpoints Admin (/api/v1/admin/clients/*)
  • Schemas de request/response
  • Códigos de status
  • Exemplos de uso

4. Atualizar Swagger/OpenAPI

Arquivo: docs/BFF_API_CONTRACT.yaml (atualizar)

Conteúdo:

  • Adicionar seção OAuth2
  • Adicionar security schemes
  • Documentar endpoints de registro
  • Exemplos de autenticação

5. Guia de Configuração e Deploy

Arquivo: docs/BFF_DEPLOYMENT_GUIDE.md

Conteúdo:

  • Requisitos de infraestrutura
  • Configuração de variáveis de ambiente
  • Docker e docker-compose
  • Health checks
  • Logging e monitoramento
  • Troubleshooting de deploy

Arquivos a atualizar:

  • docs/AVALIACAO_BFF_BACKEND_FOR_FRONTEND.md - Adicionar referência ao OAuth2
  • docs/REAVALIACAO_BFF_MODULO_VS_APLICACAO_EXTERNA.md - Atualizar com detalhes de autenticação
  • docs/00_INDEX.md - Adicionar links para nova documentação
  • README.md - Atualizar seção de arquitetura
  • Criar BFF_OAUTH2_GUIDE.md
  • Criar BFF_DEVELOPER_INTEGRATION_GUIDE.md
  • Criar BFF_API_REFERENCE.md
  • Atualizar BFF_API_CONTRACT.yaml
  • Criar BFF_DEPLOYMENT_GUIDE.md
  • Atualizar AVALIACAO_BFF_BACKEND_FOR_FRONTEND.md
  • Atualizar REAVALIACAO_BFF_MODULO_VS_APLICACAO_EXTERNA.md
  • Atualizar 00_INDEX.md
  • Atualizar README.md
  • Adicionar exemplos de código em múltiplas linguagens
  • Criar diagramas de sequência
  • Criar diagramas de arquitetura

📊 Configuração de Logs e Observabilidade

1. Configuração de Logging (Serilog)

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Arah.Api.Bff": "Information"
    },
    "Seq": {
      "ServerUrl": "http://localhost:5341",
      "ApiKey": ""
    }
  },
  "Serilog": {
    "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.Seq" ],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning",
        "Arah.Api.Bff.Security": "Debug"
      }
    },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{CorrelationId}] [{BffClientId}] {Message:lj}{NewLine}{Exception}"
        }
      },
      {
        "Name": "File",
        "Args": {
          "path": "logs/bff-.log",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 30,
          "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{CorrelationId}] [{BffClientId}] [{MachineName}] [{ThreadId}] {Message:lj}{NewLine}{Exception}"
        }
      }
    ],
    "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId", "WithEnvironmentName" ],
    "Properties": {
      "Application": "Arah.Bff",
      "Version": "1.0.0"
    }
  }
}
// Arah.Api.Bff/Program.cs
using Serilog;
using System.Reflection;

var builder = WebApplication.CreateBuilder(args);

// Serilog Configuration
builder.Host.UseSerilog((context, configuration) =>
{
    var seqUrl = context.Configuration["Logging:Seq:ServerUrl"];
    var logLevel = context.Configuration["Logging:LogLevel:Default"] ?? "Information";
    var minLevel = Enum.TryParse<Serilog.Events.LogEventLevel>(logLevel, true, out var level) 
        ? level 
        : Serilog.Events.LogEventLevel.Information;

    configuration
        .ReadFrom.Configuration(context.Configuration)
        .Enrich.FromLogContext()
        .Enrich.WithMachineName()
        .Enrich.WithThreadId()
        .Enrich.WithEnvironmentName()
        .Enrich.WithProperty("Application", "Arah.Bff")
        .Enrich.WithProperty("Version", Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown")
        .MinimumLevel.Is(minLevel)
        .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning)
        .MinimumLevel.Override("System", Serilog.Events.LogEventLevel.Warning)
        .WriteTo.Console(
            outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{CorrelationId}] [{BffClientId}] {Message:lj}{NewLine}{Exception}")
        .WriteTo.File(
            "logs/bff-.log",
            rollingInterval: RollingInterval.Day,
            retainedFileCountLimit: 30,
            outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{CorrelationId}] [{BffClientId}] [{MachineName}] [{ThreadId}] {Message:lj}{NewLine}{Exception}");

    // Add Seq sink if configured
    if (!string.IsNullOrWhiteSpace(seqUrl))
    {
        configuration.WriteTo.Seq(
            serverUrl: seqUrl,
            apiKey: context.Configuration["Logging:Seq:ApiKey"],
            restrictedToMinimumLevel: minLevel);
    }
});
// Arah.Api.Bff/Middleware/BffRequestLoggingMiddleware.cs
using Serilog.Context;
using System.Diagnostics;

namespace Arah.Api.Bff.Middleware;

public sealed class BffRequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<BffRequestLoggingMiddleware> _logger;

    public BffRequestLoggingMiddleware(
        RequestDelegate next,
        ILogger<BffRequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = Stopwatch.StartNew();
        var method = context.Request.Method;
        var path = context.Request.Path.Value ?? "/";
        var correlationId = context.Items["CorrelationId"]?.ToString() ?? Guid.NewGuid().ToString();
        var clientId = context.Items["BffClientId"]?.ToString() ?? "anonymous";

        // Enriquecer contexto de log
        using (LogContext.PushProperty("CorrelationId", correlationId))
        using (LogContext.PushProperty("BffClientId", clientId))
        using (LogContext.PushProperty("RequestMethod", method))
        using (LogContext.PushProperty("RequestPath", path))
        {
            try
            {
                await _next(context);
            }
            finally
            {
                stopwatch.Stop();
                var statusCode = context.Response.StatusCode;

                _logger.LogInformation(
                    "Request: {Method} {Path} {StatusCode} {DurationMs}ms CorrelationId: {CorrelationId} ClientId: {BffClientId}",
                    method, path, statusCode, stopwatch.ElapsedMilliseconds, correlationId, clientId);
            }
        }
    }
}

2. Logging Estruturado para Auditoria

// Arah.Api.Bff/Services/OAuth/AuditLogService.cs
public sealed class AuditLogService
{
    private readonly ILogger<AuditLogService> _logger;

    public void LogClientRegistration(
        string clientId,
        string clientName,
        Guid createdByUserId,
        IReadOnlyList<string> scopes)
    {
        _logger.LogInformation(
            "OAuth2 Client Registered: ClientId={ClientId}, Name={ClientName}, CreatedBy={CreatedByUserId}, Scopes={Scopes}",
            clientId, clientName, createdByUserId, string.Join(",", scopes));
    }

    public void LogTokenIssued(
        string clientId,
        IReadOnlyList<string> scopes,
        DateTime expiresAt)
    {
        _logger.LogInformation(
            "OAuth2 Token Issued: ClientId={ClientId}, Scopes={Scopes}, ExpiresAt={ExpiresAt}",
            clientId, string.Join(",", scopes), expiresAt);
    }

    public void LogTokenValidationFailed(string clientId, string reason)
    {
        _logger.LogWarning(
            "OAuth2 Token Validation Failed: ClientId={ClientId}, Reason={Reason}",
            clientId, reason);
    }

    public void LogClientDeactivated(string clientId, Guid deactivatedByUserId)
    {
        _logger.LogWarning(
            "OAuth2 Client Deactivated: ClientId={ClientId}, DeactivatedBy={DeactivatedByUserId}",
            clientId, deactivatedByUserId);
    }
}

3. Métricas (Prometheus)

// Arah.Api.Bff/Program.cs
using Prometheus;

// Métricas customizadas
var bffTokenIssuedCounter = Metrics.CreateCounter(
    "bff_oauth_tokens_issued_total",
    "Total number of OAuth2 tokens issued",
    new[] { "client_id", "scope" });

var bffRequestDuration = Metrics.CreateHistogram(
    "bff_request_duration_seconds",
    "Duration of BFF requests",
    new[] { "method", "endpoint", "status_code" });

var bffApiClientDuration = Metrics.CreateHistogram(
    "bff_api_client_duration_seconds",
    "Duration of API client requests to main API",
    new[] { "endpoint", "status_code" });

var bffApiClientErrors = Metrics.CreateCounter(
    "bff_api_client_errors_total",
    "Total number of API client errors",
    new[] { "endpoint", "error_type" });

// Registrar métricas no middleware
app.UseMetricServer(); // Endpoint /metrics
app.UseHttpMetrics(); // Métricas HTTP automáticas
// Arah.Api.Bff/Services/OAuth/BffTokenService.cs
public sealed class BffTokenService : IBffTokenService
{
    private readonly ILogger<BffTokenService> _logger;
    private readonly ICounter _tokenIssuedCounter;

    public string IssueClientToken(string clientId, IReadOnlyList<string> scopes)
    {
        var token = /* ... gerar token ... */;
        
        // Registrar métrica
        foreach (var scope in scopes)
        {
            _tokenIssuedCounter.WithLabels(clientId, scope).Inc();
        }
        
        _logger.LogDebug("Token issued for client {ClientId} with scopes {Scopes}", clientId, string.Join(",", scopes));
        
        return token;
    }
}

4. OpenTelemetry Tracing

// Arah.Api.Bff/Program.cs
using OpenTelemetry.Trace;
using OpenTelemetry.Resources;
using OpenTelemetry.Metrics;

builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource
        .AddService(
            serviceName: "Arah.Bff",
            serviceVersion: "1.0.0"))
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddSource("Arah.Bff")
        .AddOtlpExporter(options =>
        {
            options.Endpoint = new Uri(builder.Configuration["OpenTelemetry:Otlp:Endpoint"] ?? "");
        }))
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddPrometheusExporter());

5. Health Checks

// Arah.Api.Bff/HealthChecks/BffHealthCheck.cs
public sealed class BffHealthCheck : IHealthCheck
{
    private readonly IClientApplicationRepository _clientRepository;
    private readonly ApiHttpClient _apiClient;
    private readonly ILogger<BffHealthCheck> _logger;

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            // Verificar acesso ao banco de dados
            var clients = await _clientRepository.ListAsync(cancellationToken);
            
            // Verificar conectividade com API principal
            var apiHealth = await _apiClient.GetHealthAsync(cancellationToken);
            
            if (!apiHealth.IsHealthy)
            {
                return HealthCheckResult.Degraded(
                    "BFF is running but API principal is unavailable",
                    data: new Dictionary<string, object>
                    {
                        ["api_principal_status"] = apiHealth.Status
                    });
            }
            
            return HealthCheckResult.Healthy(
                "BFF is healthy",
                data: new Dictionary<string, object>
                {
                    ["registered_clients"] = clients.Count,
                    ["api_principal_status"] = apiHealth.Status
                });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Health check failed");
            return HealthCheckResult.Unhealthy("BFF health check failed", ex);
        }
    }
}

// Registrar health checks
builder.Services.AddHealthChecks()
    .AddCheck<BffHealthCheck>("bff")
    .AddCheck<DatabaseHealthCheck>("database")
    .AddUrlGroup(new Uri(builder.Configuration["ApiPrincipal:BaseUrl"] + "/health"), "api_principal");

6. Configuração de Ambiente

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "Arah.Api.Bff": "Debug"
    },
    "Seq": {
      "ServerUrl": "http://localhost:5341"
    }
  },
  "ApiPrincipal": {
    "BaseUrl": "http://localhost:5000",
    "TimeoutSeconds": 30
  },
  "Bff": {
    "Jwt": {
      "Issuer": "Arah.Bff",
      "Audience": "Arah.Bff",
      "SigningKey": "dev-only-change-me-in-production-min-32-chars",
      "ExpirationMinutes": 60
    }
  }
}
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Arah.Api.Bff": "Information"
    },
    "Seq": {
      "ServerUrl": "${SEQ_SERVER_URL}",
      "ApiKey": "${SEQ_API_KEY}"
    }
  },
  "ApiPrincipal": {
    "BaseUrl": "${API_PRINCIPAL_BASE_URL}",
    "TimeoutSeconds": 30
  },
  "Bff": {
    "Jwt": {
      "Issuer": "Arah.Bff",
      "Audience": "Arah.Bff",
      "SigningKey": "${BFF_JWT_SIGNING_KEY}",
      "ExpirationMinutes": 60
    }
  },
  "OpenTelemetry": {
    "Otlp": {
      "Endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}"
    }
  }
}

7. Dashboards e Alertas

7.1 Métricas Principais

  • Taxa de tokens emitidos (bff_oauth_tokens_issued_total)
  • Latência de requisições BFF (bff_request_duration_seconds)
  • Latência de chamadas à API principal (bff_api_client_duration_seconds)
  • Taxa de erros (bff_api_client_errors_total)
  • Clientes ativos (query no banco)

7.2 Alertas Recomendados

  • Alta taxa de erros: > 5% de requisições falhando
  • Latência alta: P95 > 1s
  • API principal indisponível: Health check falhando
  • Muitos tokens inválidos: > 10% de tokens rejeitados
  • Cliente desativado tentando usar: Tentativas de uso após desativação

Este plano detalha a extração do BFF para uma aplicação externa com:

  1. Autenticação própria (OAuth2 Client Credentials)
  2. Registro de múltiplos apps (sistema de clientes OAuth2)
  3. Consumo da API principal (HTTP com repasse de token)
  4. Escalabilidade independente (aplicação separada)
  5. Segurança (hash de secrets, validação de tokens, rate limiting)
  6. Observabilidade completa (logs estruturados, métricas, tracing)
  7. Documentação abrangente (guias, referências, exemplos)

Esforço Total: 6 semanas (240 horas)

Próximos Passos:

  1. Aprovar plano
  2. Iniciar Fase 1 (Preparação)
  3. Implementar em sprints de 1 semana
  4. Testar incrementalmente
  5. Documentar durante implementação

Última Atualização: 2026-01-28
Status: 📋 Plano Completo - Pronto para Implementação