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)
| Claim | Tipo | Descripción |
|---|---|---|
sub | string | User ID en Keycloak |
tenant_id | string (UUID) | Tenant del usuario |
organization_id | string (UUID) | Organización del usuario |
roles | string[] | Roles asignados |
email | string | Email del usuario |
name | string | Nombre 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
IIdentityContextse construye desdeHttpContext.User.Claims
Request HTTP servicio-a-servicio
Servicio A → [JWT propagado en Authorization header] → Servicio BEl 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)
- Nunca acceder a claims directamente — siempre via
IIdentityContext - Nunca confiar en headers custom (X-Tenant-Id) — siempre JWT firmado
- Siempre propagar JWT en llamadas servicio-a-servicio
- Eventos siempre incluyen TenantId — el consumer no debe adivinarlo
- RLS se establece desde IIdentityContext — nunca hardcodear tenant_id
- Endpoints públicos (ejecución, wizard) autentican por access_token, no JWT
Resumen de Fuentes por Escenario
| Escenario | Fuente de TenantId | Fuente de UserId | ActorType |
|---|---|---|---|
| HTTP externo (con JWT) | JWT claim tenant_id | JWT claim sub | user |
| HTTP servicio-a-servicio | JWT propagado | JWT propagado | user |
| Consumer de evento | message.TenantId | message.ActorId | system |
| Job programado | payload.TenantId | — | scheduler |
| Ejecución pública | request.TenantId | — | anonymous |