Skip to content

Contexto de Identidad y Propagación de JWT

Principio

Cada request que llega a un servicio debe tener un contexto de identidad que identifica:

  • Quién ejecuta la acción (UserId)
  • En qué tenant opera (TenantId)
  • En qué organización opera (OrganizationId)
  • Con qué permisos (Roles)

Este contexto se obtiene del JWT firmado por Keycloak y se expone via IIdentityContext.

Interfaz Principal

csharp
public interface IIdentityContext
{
    Guid TenantId { get; }
    Guid OrganizationId { get; }
    string UserId { get; }
    string[] Roles { get; }
    string ActorType { get; } // "user" | "system" | "scheduler" | "anonymous"
    bool IsAuthenticated { get; }
    
    // Para consumers y jobs (override manual)
    void SetContext(Guid tenantId, string userId = "", string actorType = "system");
}

Claims del JWT (Keycloak)

ClaimTipoDescripción
substringUser ID en Keycloak
tenant_idstring (UUID)Tenant del usuario
organization_idstring (UUID)Organización del usuario
rolesstring[]Roles asignados
emailstringEmail del usuario
namestringNombre completo

Propagación de JWT

Request HTTP externo (cliente → Gateway → servicio)

Cliente → [JWT en Authorization header] → YARP Gateway → [JWT propagado] → Servicio
  • YARP valida el JWT (firma, expiración, issuer)
  • YARP propaga el JWT completo al servicio interno
  • El servicio valida el JWT con middleware estándar de .NET
  • IIdentityContext se construye desde HttpContext.User.Claims

Request HTTP servicio-a-servicio

Servicio A → [JWT propagado en Authorization header] → Servicio B

El servicio que llama a otro propaga el JWT original:

csharp
public class JwtPropagationHandler : DelegatingHandler
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken ct)
    {
        var authHeader = _httpContextAccessor.HttpContext?
            .Request.Headers.Authorization.FirstOrDefault();

        if (!string.IsNullOrEmpty(authHeader))
            request.Headers.TryAddWithoutValidation("Authorization", authHeader);

        return base.SendAsync(request, ct);
    }
}

Consumer de eventos (RabbitMQ)

No hay JWT. El contexto se obtiene del payload del mensaje:

csharp
_identity.SetContext(message.TenantId, message.ActorId, message.ActorType);

Job programado

No hay JWT. El contexto se obtiene del payload del job:

csharp
_identity.SetContext(payload.TenantId, actorType: "scheduler");

Ejecución pública (usuario final sin JWT)

El usuario final se autentica con access_token de la solicitud:

csharp
_identity.SetContext(request.TenantId, actorType: "anonymous");

Integración con RLS

csharp
public class TenantDbConnectionInterceptor : DbConnectionInterceptor
{
    private readonly IIdentityContext _identity;

    public override async Task ConnectionOpenedAsync(
        DbConnection connection, ConnectionEndEventData eventData, CancellationToken ct)
    {
        if (_identity.TenantId != Guid.Empty)
        {
            await using var cmd = connection.CreateCommand();
            cmd.CommandText = $"SET app.current_tenant_id = '{_identity.TenantId}'";
            await cmd.ExecuteNonQueryAsync(ct);
        }
    }
}

Reglas (NO negociables)

  1. Nunca acceder a claims directamente — siempre via IIdentityContext
  2. Nunca confiar en headers custom (X-Tenant-Id) — siempre JWT firmado
  3. Siempre propagar JWT en llamadas servicio-a-servicio
  4. Eventos siempre incluyen TenantId — el consumer no debe adivinarlo
  5. RLS se establece desde IIdentityContext — nunca hardcodear tenant_id
  6. Endpoints públicos (ejecución, wizard) autentican por access_token, no JWT

Resumen de Fuentes por Escenario

EscenarioFuente de TenantIdFuente de UserIdActorType
HTTP externo (con JWT)JWT claim tenant_idJWT claim subuser
HTTP servicio-a-servicioJWT propagadoJWT propagadouser
Consumer de eventomessage.TenantIdmessage.ActorIdsystem
Job programadopayload.TenantIdscheduler
Ejecución públicarequest.TenantIdanonymous

Reimagine Tech LLC — Documentacion Interna