BTW.KeycloakJwtGenerator
1.1.0
dotnet add package BTW.KeycloakJwtGenerator --version 1.1.0
NuGet\Install-Package BTW.KeycloakJwtGenerator -Version 1.1.0
<PackageReference Include="BTW.KeycloakJwtGenerator" Version="1.1.0" />
<PackageVersion Include="BTW.KeycloakJwtGenerator" Version="1.1.0" />
<PackageReference Include="BTW.KeycloakJwtGenerator" />
paket add BTW.KeycloakJwtGenerator --version 1.1.0
#r "nuget: BTW.KeycloakJwtGenerator, 1.1.0"
#:package BTW.KeycloakJwtGenerator@1.1.0
#addin nuget:?package=BTW.KeycloakJwtGenerator&version=1.1.0
#tool nuget:?package=BTW.KeycloakJwtGenerator&version=1.1.0
📦 BTW.KeycloakJwtGenerator
Librería .NET para generar JWT centralizados usando Keycloak como Identity Provider. Permite que sistemas con autenticación propia (LDAP, Base de Datos, Active Directory, etc.) generen tokens JWT estandarizados con claims personalizados.
🎯 ¿Para Qué Sirve?
Tu Sistema (auth propia) → BTW.KeycloakJwtGenerator → Keycloak → JWT con tus claims
- ✅ Tu sistema mantiene su mecanismo de autenticación (BD, LDAP, AD)
- ✅ Keycloak genera JWT estandarizados
- ✅ Incluye los claims personalizados que necesites
- ✅ Todos tus sistemas validan el mismo JWT
📦 Instalación
dotnet add package BTW.KeycloakJwtGenerator
⚡ Requisitos
Compatible con .NET 6.0, .NET 7.0, .NET 8.0 y .NET 10.0.
¿Otros targets?
- .NET Core 3.1 →
BTW.KeycloakJwtGenerator.NetCore31- .NET Framework 4.6.1 (Web API 2 + OWIN) →
BTW.KeycloakJwtGenerator.NetFramework461
🚀 Uso Rápido
1. Configurar en appsettings.json
{
"KeycloakJwt": {
"BaseUrl": "http://localhost:8080",
"Realm": "mi-realm",
"ClientId": "mi-sistema",
"ClientSecret": "mi-secret",
"SystemName": "Sistema A"
}
}
2. Registrar en Program.cs
// Opción 1: Desde appsettings.json
builder.Services.AddKeycloakJwtGenerator(builder.Configuration);
// Opción 2: Configuración fluida
builder.Services
.AddKeycloakJwt()
.WithBaseUrl("http://localhost:8080")
.WithRealm("mi-realm")
.WithClient("mi-cliente", "mi-secret")
.WithSystemName("Sistema A")
.Build();
// Opción 3: Con Action
builder.Services.AddKeycloakJwtGenerator(options =>
{
options.BaseUrl = "http://localhost:8080";
options.Realm = "mi-realm";
options.ClientId = "mi-cliente";
options.ClientSecret = "mi-secret";
options.SystemName = "Sistema A";
});
3. Usar en tu código
public class AuthController : ControllerBase
{
private readonly IKeycloakJwtGenerator _jwtGenerator;
public AuthController(IKeycloakJwtGenerator jwtGenerator)
{
_jwtGenerator = jwtGenerator;
}
[HttpPost("login")]
public async Task<IActionResult> Login(LoginRequest request)
{
// 1. Tu autenticación local (BD, LDAP, etc.)
var user = await _myAuthService.ValidateAsync(request.Username, request.Password);
if (user == null)
return Unauthorized();
// 2. Definir claims personalizados
var claims = new Dictionary<string, object>
{
["departamento"] = user.Departamento,
["nivel_acceso"] = user.NivelAcceso,
["permisos"] = user.Permisos, // Array
["sucursal"] = user.Sucursal,
["metadata"] = new { // Objeto anidado
zona = user.Zona,
codigo = user.Codigo
}
};
// 3. Generar JWT con Keycloak
var token = await _jwtGenerator.GenerateTokenAsync(request.Username, claims);
return Ok(new
{
accessToken = token.AccessToken,
refreshToken = token.RefreshToken,
expiresIn = token.ExpiresIn
});
}
}
📋 API Completa
Generar Token (Simple)
var token = await _jwtGenerator.GenerateTokenAsync(
"usuario",
new Dictionary<string, object>
{
["departamento"] = "ventas",
["permisos"] = new[] { "leer", "escribir" }
});
Generar Token (Request Completo)
var token = await _jwtGenerator.GenerateTokenAsync(new GenerateTokenRequest
{
Username = "usuario",
Email = "usuario@empresa.com",
FirstName = "Juan",
LastName = "Pérez",
Roles = new List<string> { "user", "admin" },
Scope = "admin:dictionary", // scopes OAuth per-request (opcional)
Claims = new Dictionary<string, object>
{
["departamento"] = "ventas"
}
});
Solicitar Scopes per-request (scope)
El campo Scope (string separado por espacios) se envía como parámetro scope en
el grant de Keycloak. Sirve para incluir Client Scopes de tipo Optional en el claim
scope del token solo cuando se piden, sin dejar atributos persistidos en el usuario.
// Con scope → el access token incluye "admin:dictionary" en el claim "scope"
await _jwtGenerator.GenerateTokenAsync(new GenerateTokenRequest
{
Username = "usuario",
Scope = "admin:dictionary inventory:read"
});
// Sin Scope → el token no lleva esos scopes
await _jwtGenerator.GenerateTokenAsync(new GenerateTokenRequest { Username = "usuario" });
Requiere que el scope esté dado de alta como Optional Client Scope del client (ver Configuración de Keycloak) y Token Exchange habilitado.
Refrescar Token
var newToken = await _jwtGenerator.RefreshTokenAsync(refreshToken);
Validar Token
bool isValid = await _jwtGenerator.ValidateTokenAsync(accessToken);
Revocar Token (Logout)
await _jwtGenerator.RevokeTokenAsync(refreshToken);
🔗 gRPC — Propagación del JWT entre Microservicios
Para arquitecturas donde Microservicio A llama a Microservicio B vía gRPC usando el mismo JWT del usuario:
1. Registrar el handler
// Program.cs / Startup.cs
builder.Services.AddKeycloakJwtValidation(builder.Configuration); // valida JWTs entrantes
builder.Services.AddKeycloakGrpcForwarding(); // propaga JWT a llamadas salientes
2. Adjuntarlo al cliente gRPC
builder.Services
.AddGrpcClient<OrderService.OrderServiceClient>(o =>
o.Address = new Uri("https://orders-service"))
.AddHttpMessageHandler<JwtForwardingHandler>();
3. Proteger el servicio receptor con [Authorize]
// En el Microservicio B (servicio gRPC receptor)
builder.Services.AddKeycloakJwtValidation(builder.Configuration);
// En el servicio gRPC
public class OrderGrpcService : OrderService.OrderServiceBase
{
[Authorize]
public override Task<OrderReply> GetOrder(OrderRequest request, ServerCallContext context)
{
// Claims del JWT entrante disponibles vía User
var departamento = context.GetHttpContext().User.FindFirst("departamento")?.Value;
return Task.FromResult(new OrderReply { ... });
}
}
Cómo funciona:
JwtForwardingHandlerlee la cabeceraAuthorizationde la petición HTTP entrante (víaIHttpContextAccessor) y la añade automáticamente a todas las llamadas gRPC salientes del servicio. El JWT lo firma Keycloak, por lo que es válido en cualquier servicio que apunte al mismo realm.
| Rol del servicio | Método |
|---|---|
| Emisor (genera JWT) | AddKeycloakJwtGenerator() |
| Intermediario (valida + reenvía) | AddKeycloakJwtValidation() + AddKeycloakGrpcForwarding() |
| Receptor (solo valida) | AddKeycloakJwtValidation() |
🔐 Validar JWT en Otros Sistemas
Para sistemas que solo necesitan validar tokens (no generarlos):
// Solo agrega validación de JWT
builder.Services.AddKeycloakJwtValidation(builder.Configuration);
// O ambos (generar y validar)
builder.Services.AddKeycloakJwtFull(builder.Configuration);
Luego usa [Authorize] normalmente:
[Authorize]
[HttpGet("protected")]
public IActionResult Protected()
{
var departamento = User.FindFirst("departamento")?.Value;
return Ok(new { departamento });
}
⚙️ Configuración Completa
{
"KeycloakJwt": {
"BaseUrl": "http://localhost:8080",
"Realm": "mi-realm",
"ClientId": "mi-sistema",
"ClientSecret": "secret",
"SystemName": "Sistema A",
"TimeoutSeconds": 30,
"EnableAdminTokenCache": true,
"AdminTokenCacheMinutes": 4,
"UseTokenExchange": true,
"MaxRetries": 3
}
}
| Opción | Descripción | Default |
|---|---|---|
BaseUrl |
URL de Keycloak | http://localhost:8080 |
Realm |
Nombre del Realm | master |
ClientId |
Client ID del service account | - |
ClientSecret |
Client Secret | - |
SystemName |
Nombre del sistema (claim) | null |
TimeoutSeconds |
Timeout HTTP | 30 |
EnableAdminTokenCache |
Cachear token admin | true |
AdminTokenCacheMinutes |
Tiempo de caché | 4 |
UseTokenExchange |
Usar Token Exchange | true |
MaxRetries |
Reintentos en fallo | 3 |
🛠️ Configuración de Keycloak
1. Crear Client (Service Account)
Clients → Create client
├── Client ID: "mi-sistema"
├── Client authentication: ON
├── Service accounts roles: ✅
└── Save
2. Asignar Roles al Service Account
Client → Service account roles → Assign role
├── manage-users
├── view-users
└── impersonation (para Token Exchange)
3. Crear Protocol Mappers
Client → Client scopes → Dedicated scope → Mappers
├── Mapper type: "User Attribute"
├── User Attribute: "departamento"
├── Token Claim Name: "departamento"
├── Add to access token: ✅
└── Save
4. Habilitar Token Exchange (opcional)
Si estás ejecutando Keycloak con Docker, agrega esta configuración para habilitar Token Exchange:
# docker-compose.yml (configuración de Keycloak, no de tu aplicación)
services:
keycloak:
image: quay.io/keycloak/keycloak:latest
environment:
KC_FEATURES: token-exchange
# ... otras configuraciones
Nota: Esta configuración es solo para Keycloak. Si Keycloak ya tiene Token Exchange habilitado o usas otra forma de instalación, puedes omitir este paso.
5. Client Scopes Opcionales (per-request)
Para usar el campo Scope del request (incluir un scope en el token solo cuando se pide):
Client scopes → Create client scope
├── Name: admin:dictionary
├── Type: Optional
├── Protocol: openid-connect
└── Save
Clients → tu client → Client scopes → Add client scope
├── admin:dictionary
└── Assigned type: Optional
Un scope Optional solo se incluye en el token cuando se envía en el parámetro scope
(lo que hace GenerateTokenRequest.Scope). Si lo asignas como Default, irá en todos los
tokens del client y Scope deja de tener efecto diferenciador.
📄 Licencia
MIT License - ver LICENSE
Desarrollado con ❤️ por By The Wave (BTW)
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.2)
- Microsoft.Extensions.Caching.Memory (>= 10.0.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.2)
- Microsoft.Extensions.Http (>= 10.0.2)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.2)
- Microsoft.Extensions.Options (>= 10.0.2)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.2)
- System.IdentityModel.Tokens.Jwt (>= 8.15.0)
-
net6.0
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 6.0.35)
- Microsoft.Extensions.Caching.Memory (>= 8.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.3)
- Microsoft.Extensions.Options (>= 8.0.2)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
- System.IdentityModel.Tokens.Jwt (>= 8.15.0)
- System.Net.Http.Json (>= 8.0.1)
- System.Text.Json (>= 8.0.6)
-
net7.0
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 7.0.20)
- Microsoft.Extensions.Caching.Memory (>= 8.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.3)
- Microsoft.Extensions.Options (>= 8.0.2)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
- System.IdentityModel.Tokens.Jwt (>= 8.15.0)
- System.Net.Http.Json (>= 8.0.1)
- System.Text.Json (>= 8.0.6)
-
net8.0
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 8.0.11)
- Microsoft.Extensions.Caching.Memory (>= 8.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.3)
- Microsoft.Extensions.Options (>= 8.0.2)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
- System.IdentityModel.Tokens.Jwt (>= 8.15.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.