DS.Foundation.Authentication
8.4.2
dotnet add package DS.Foundation.Authentication --version 8.4.2
NuGet\Install-Package DS.Foundation.Authentication -Version 8.4.2
<PackageReference Include="DS.Foundation.Authentication" Version="8.4.2" />
<PackageVersion Include="DS.Foundation.Authentication" Version="8.4.2" />
<PackageReference Include="DS.Foundation.Authentication" />
paket add DS.Foundation.Authentication --version 8.4.2
#r "nuget: DS.Foundation.Authentication, 8.4.2"
#:package DS.Foundation.Authentication@8.4.2
#addin nuget:?package=DS.Foundation.Authentication&version=8.4.2
#tool nuget:?package=DS.Foundation.Authentication&version=8.4.2
DS.Foundation.Authentication
Emisión y validación de JWT firmados con HMAC-SHA256, el modelo de usuario autenticado (UserToken) y la validación de API keys, sin depender de ASP.NET Core.
Úselo en el servicio que emite tokens (por ejemplo DS.Security) y en cualquier proceso que deba validar un token fuera del pipeline HTTP. Para proteger endpoints de ASP.NET Core instale además DS.Foundation.Authentication.AspNetCore.
- Target framework:
netstandard2.0. - Dependencias:
DS.Foundation,Microsoft.IdentityModel.JsonWebTokens,Microsoft.Extensions.Options.ConfigurationExtensions.
Instalación
<PackageReference Include="DS.Foundation.Authentication" Version="8.4.2" />
Registro
using DS.Foundation.Authentication.Configuration;
using DS.Foundation.Services;
using DS.Foundation.Services.Extensions;
// Enlaza JwtAuthenticationOptions con la sección System:Authentication:Jwt.
builder.Services.AddJwtAuthentication(builder.Configuration);
// Registra JsonWebTokenService y OmniLayerCookieService como scoped.
builder.Services.AddFoundationServices(new IServiceRepository[]
{
new DS.Foundation.Authentication.Services.ServiceRepository(),
});
AddJwtAuthentication solo enlaza las opciones: no valida la configuración al arrancar. La validación ocurre al emitir o validar el primer token.
Configuración
Sección System:Authentication:Jwt (JwtAuthenticationOptions.SectionName):
| Clave | Tipo | Valor por defecto | Obligatoria |
|---|---|---|---|
Issuer |
string |
"" |
Sí, salvo que ValidIssuers tenga al menos una entrada |
ValidIssuers |
List<string> |
vacía | Sí, salvo que Issuer tenga valor |
Audience |
string |
"" |
Sí |
SecretKey |
string |
"" |
Sí, al menos 32 bytes en UTF-8 |
TokenLifetimeMinutes |
int |
60 |
No |
ClockSkewSeconds |
int |
30 |
No |
RequireHttpsMetadata |
bool |
true |
No (lo usa el paquete AspNetCore) |
{
"System": {
"Authentication": {
"Jwt": {
"Issuer": "ds-security",
"Audience": "mi-sistema",
"SecretKey": "<se carga desde variables de entorno o un almacén de secretos>",
"TokenLifetimeMinutes": 60,
"ClockSkewSeconds": 30
}
}
}
}
El emisor y todas las aplicaciones que validan sus tokens deben compartir Audience y SecretKey, y el iss de cada token debe ser uno de los emisores aceptados por la aplicación que lo valida.
Varios emisores: Issuer y ValidIssuers
Desde 8.4.1 una aplicación puede aceptar tokens de más de un emisor, por ejemplo durante el cambio de DS.Security a https://security.<dominio>.
- Emisores aceptados (
JwtAuthenticationOptions.GetAcceptedIssuers()):Issuer, si tiene valor, más las entradas deValidIssuers. Las dos claves valen lo mismo para validar. Los valores se recortan, los vacíos se ignoran y los repetidos se cuentan una vez. - La comparación con el
issdel token es exacta y distingue mayúsculas:https://Security.testno coincide conhttps://security.test, yhttps://security.test/tampoco. - Hace falta al menos una de las dos claves. Si no queda ningún emisor,
Validate()lanzaInvalidOperationException: System:Authentication:Jwt:Issuer or System:Authentication:Jwt:ValidIssuers is required. - Emisor con el que se firma (
JwtAuthenticationOptions.GetDefaultIssuer(), usado porCreateJwtFromToken(token)):Issuersi tiene valor; si no, la primera entrada válida deValidIssuers, en el orden de la configuración. Para firmar con otro emisor aceptado useCreateJwtFromToken(token, issuer). - Una aplicación configurada solo con
Issuerfunciona igual que antes, sin cambios.
{
"System": {
"Authentication": {
"Jwt": {
"Issuer": "DS.Security",
"ValidIssuers": [ "https://security.example.com" ],
"Audience": "mi-sistema"
}
}
}
}
Con variables de entorno (por ejemplo en Docker), cada entrada de la lista lleva su índice:
System__Authentication__Jwt__Issuer=DS.Security
System__Authentication__Jwt__ValidIssuers__0=https://security.example.com
System__Authentication__Jwt__ValidIssuers__1=https://security.otro-dominio.com
IConfiguration combina las listas por índice: ValidIssuers__0 en una variable de entorno reemplaza la entrada 0 de appsettings.json, y las demás entradas se conservan.
Importante: una aplicación configurada solo con
ValidIssuers(sinIssuer) necesita 8.4.1 o posterior. Las versiones anteriores ignoranValidIssuers, exigenIssuery fallan al arrancar o al validar el primer token.
Cambiar el emisor de los tokens
- En cada aplicación que valida tokens, actualice el paquete a 8.4.1 o posterior y agregue el emisor nuevo a
ValidIssuers, manteniendo el actual enIssuer. Despliegue todas. - Recién cuando todas las aplicaciones aceptan el emisor nuevo, el emisor (DS.Security) empieza a firmar con él: con
CreateJwtFromToken(token, "https://security.example.com")en los endpoints que lo necesiten, o cambiando suIssuer. - Cuando ya no queden tokens vigentes con el emisor anterior (pasado
TokenLifetimeMinutes), puede quitarlo de la configuración de las aplicaciones.
Si se invierte el orden, las aplicaciones que todavía no aceptan el emisor nuevo rechazan esos tokens (401 en ASP.NET Core, false en TryValidateAndCreateUserToken).
API key (ApiKeyValidator):
| Clave | Tipo | Valor por defecto | Obligatoria |
|---|---|---|---|
System:ApiKey |
string |
sin valor | Sí para aceptar requests: sin valor, ninguna API key es válida |
Uso
Emitir un token
using DS.Foundation.Authentication;
using DS.Foundation.Authentication.Services;
var userToken = new UserToken
{
UserId = 42,
Username = "jperez",
TenantCode = "ACME",
Roles = new List<string> { "admin" },
ApplicationRoles = new List<UserApplicationRole>
{
new UserApplicationRole { ApplicationCode = "CRM", Roles = new List<string> { "ventas" } },
},
};
string jwt = jsonWebTokenService.CreateJwtFromToken(userToken);
// Con un emisor específico; debe ser uno de los emisores aceptados.
string jwtOauth = jsonWebTokenService.CreateJwtFromToken(userToken, "https://security.example.com");
CreateJwtFromToken(token) firma con GetDefaultIssuer() (ver Varios emisores). CreateJwtFromToken(token, issuer) lanza ArgumentException si issuer está vacío o no coincide exactamente (sin recortar y distinguiendo mayúsculas) con uno de GetAcceptedIssuers().
El token incluye estos claims:
ClaimTypes.NameIdentifier(UserId),ClaimTypes.Name(Username) yClaimTypes.GroupSid(TenantCode).- Un
ClaimTypes.Rolepor cada rol general (Roles). En el JWT, un rol queda como texto y varios como arreglo, por ejemplo["admin","ventas"]. Así funcionanUser.IsInRole("admin")y[Authorize(Roles = "admin")]. ApplicationRoles, solo si hay roles de aplicación. Lleva un objeto JSON por aplicación, por ejemplo{"A":"CRM","R":["ventas"]}. Con una aplicación el valor es ese objeto; con varias, un arreglo de objetos. Estos roles no se agregan aClaimTypes.Role.
Las fechas iat y exp se calculan en UTC.
Validar un token
if (jsonWebTokenService.TryValidateAndCreateUserToken(jwt, out UserToken token))
{
// Firma, emisor (cualquiera de GetAcceptedIssuers()), audiencia y expiración válidos.
}
TryValidateAndCreateUserToken devuelve false si el token está vacío, mal formado, vencido, firmado con otra clave, con un emisor no aceptado o con un claim ApplicationRoles que no es JSON válido. Al reconstruir el token:
UserCodetoma el valor deUserIdcomo texto.Rolesacepta un claim por rol o el formato anterior a 8.4.0, un único claim con los roles separados por|. Los roles se recortan y no se repiten (sin distinguir mayúsculas), así que un rol no puede contener|.- Las entradas de
ApplicationRolescon el mismoApplicationCode(sin distinguir mayúsculas) se unen en una sola, sin roles repetidos.
Roles por aplicación
List<string> rolesEnCrm = token.ApplicationRoles
.Where(x => string.Equals(x.ApplicationCode, "CRM", StringComparison.OrdinalIgnoreCase))
.SelectMany(x => x.Roles)
.ToList();
Los tokens emitidos por versiones anteriores a 8.4.0 no traen el claim, y ApplicationRoles queda vacío.
Leer el usuario desde un ClaimsPrincipal
var helper = new UserHelper(principal);
int id = helper.UserId; // 0 si no hay claim
bool puede = helper.IsInRoles("admin", "ventas");
List<string> rolesEnCrm = helper.GetApplicationRoles("CRM"); // vacía si no hay claim ApplicationRoles
UserToken token = UserTokenMapper.FromClaimsPrincipal(principal); // null si no está autenticado
Replicar el JWT en una cookie (login MVC)
Las aplicaciones MVC que validan el JWT de DS.Security en el login y después trabajan con una cookie deben armar la identidad de la cookie con UserTokenMapper.ToClaims o UserTokenMapper.ToClaimsPrincipal. Es la forma recomendada: producen exactamente los mismos claims y en el mismo formato que CreateJwtFromToken, así que UserHelper (Roles, ApplicationRoles, GetApplicationRoles), UserTokenMapper.FromClaimsPrincipal y el menú de DS.Foundation.WebApp (MainMenuOptions.ApplicationCode) funcionan igual sobre la cookie que sobre el JWT.
using DS.Foundation.Authentication;
using DS.Foundation.Authentication.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
public class AccountController : Controller
{
private readonly JsonWebTokenService _jsonWebTokenService;
public AccountController(JsonWebTokenService jsonWebTokenService) => _jsonWebTokenService = jsonWebTokenService;
[AllowAnonymous]
[HttpPost]
public async Task<IActionResult> Login(string jwt, string returnUrl)
{
if (!_jsonWebTokenService.TryValidateAndCreateUserToken(jwt, out UserToken userToken))
{
return Unauthorized();
}
var principal = UserTokenMapper.ToClaimsPrincipal(userToken, CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
return LocalRedirect(string.IsNullOrEmpty(returnUrl) ? "/" : returnUrl);
}
}
Si la aplicación agrega claims propios, parta de ToClaims:
var claims = new List<Claim>(UserTokenMapper.ToClaims(userToken))
{
new Claim("Nickname", nickname),
};
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));
ToClaims devuelve, en orden:
ClaimTypes.NameIdentifier(UserId),ClaimTypes.Name(Username) yClaimTypes.GroupSid(TenantCode). Un valornullse escribe como texto vacío.- Un
ClaimTypes.Rolepor rol general, recortado, sin vacíos ni repetidos (sin distinguir mayúsculas). Un valor con|se separa en varios roles. - Un claim
UserTokenMapper.ApplicationRolesClaimType("ApplicationRoles") por cada entrada deApplicationRoles, con el JSON deUserApplicationRole({"A":"COM","R":["COMPRADOR"]}) y tipo de valorJsonClaimValueTypes.Json. No se fusionan al escribir: las entradas repetidas con distinto casing se fusionan al leer.
UserCode no se escribe porque al leer se toma de UserId. ToClaims lanza ArgumentNullException si el token es null. ToClaimsPrincipal además lanza ArgumentException si authenticationType está vacío, porque una identidad sin tipo de autenticación no queda autenticada y FromClaimsPrincipal devolvería null.
Validar una API key
bool valida = apiKeyValidator.IsValid(apiKeyRecibida); // compara con System:ApiKey
Errores frecuentes
| Síntoma | Causa | Solución |
|---|---|---|
InvalidOperationException: System:Authentication:Jwt:SecretKey must be at least 32 bytes. |
Clave corta | Usar una clave aleatoria de 32 bytes o más |
InvalidOperationException: System:Authentication:Jwt:Issuer or System:Authentication:Jwt:ValidIssuers is required. |
No hay Issuer ni entradas con valor en ValidIssuers |
Configurar al menos una de las dos claves |
InvalidOperationException: ... is required. al emitir un token |
Falta Audience o SecretKey |
Completar la sección System:Authentication:Jwt |
ArgumentException en CreateJwtFromToken(token, issuer) |
issuer vacío o distinto de todos los emisores aceptados (mayúsculas, espacios o / final incluidos) |
Agregar el emisor a Issuer o ValidIssuers, o corregir el valor |
TryValidateAndCreateUserToken devuelve siempre false |
El iss del token no está entre los emisores aceptados, Audience o SecretKey difieren, o los relojes difieren más que ClockSkewSeconds |
Alinear la configuración y la hora de los servidores |
Una aplicación configurada solo con ValidIssuers falla con ...:Issuer is required. |
Usa una versión anterior a 8.4.1 | Actualizar el paquete a 8.4.1 o posterior |
OverflowException en UserHelper.UserId |
UserId mayor que int.MaxValue (UserToken.UserId es long) |
Usar UserTokenMapper.FromClaimsPrincipal |
ApiKeyValidator.IsValid devuelve siempre false |
System:ApiKey no está configurada |
Configurar la clave |
Qué no hacer
- No guardar
SecretKeyenappsettings.jsonversionado ni en el código: use variables de entorno o un almacén de secretos. - No usar
JsonWebTokenService.CreateTokenFromJwt: está obsoleto y lanzaSecurityTokenValidationExceptionen lugar de devolverfalse. UseTryValidateAndCreateUserToken. - No usar
OmniLayerCookieService.GetCurrentJwt(): devuelve siempre una cadena vacía. - No leer los claims de un JWT sin validarlo antes.
- No armar a mano los claims de una cookie ni serializar
ApplicationRolespor su cuenta, ni repetir el literal"ApplicationRoles": useUserTokenMapper.ToClaimsoToClaimsPrincipaly, si necesita el tipo de claim,UserTokenMapper.ApplicationRolesClaimType. Si el formato cambia en una versión futura, el código propio deja de funcionar sin ningún error visible. - No empezar a emitir con un emisor nuevo antes de que todas las aplicaciones que validan los tokens lo acepten.
Cambios
Ver CHANGELOG.md.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. 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 was computed. 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 was computed. 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 was computed. 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. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- DS.Foundation (>= 8.4.1)
- Microsoft.Extensions.Configuration.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Configuration.Binder (>= 8.0.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Options (>= 8.0.2)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
- Microsoft.IdentityModel.JsonWebTokens (>= 8.2.1)
- Microsoft.IdentityModel.Tokens (>= 8.2.1)
- System.Text.Json (>= 8.0.6)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on DS.Foundation.Authentication:
| Package | Downloads |
|---|---|
|
DS.Foundation.WebApp
Reusable ASP.NET Core WebApp UI components (MainMenu ViewComponent, tag helpers). |
|
|
DS.Foundation.Authentication.AspNetCore
Integración ASP.NET Core: JwtBearer, API key y autorización. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 8.4.2 | 158 | 10/20/2026 |
| 8.4.1 | 146 | 10/20/2026 |
| 8.4.0 | 199 | 10/19/2026 |
| 8.3.3 | 145 | 10/8/2026 |
| 8.3.2 | 329 | 6/1/2026 |
| 8.3.1 | 166 | 5/31/2026 |
| 8.1.28 | 196 | 5/27/2026 |
| 8.1.27 | 229 | 3/28/2026 |
| 8.1.26 | 798 | 4/21/2025 |
| 8.1.25 | 316 | 4/9/2025 |
| 8.1.5 | 265 | 2/8/2025 |
| 8.1.4 | 285 | 2/4/2025 |
| 8.1.3 | 290 | 10/11/2024 |
| 8.1.2 | 276 | 9/23/2024 |
| 8.1.1 | 308 | 8/3/2024 |
| 8.1.0 | 286 | 7/16/2024 |
| 8.0.26 | 332 | 6/25/2024 |
| 8.0.25 | 267 | 6/25/2024 |
| 8.0.24 | 299 | 6/19/2024 |
| 8.0.23 | 285 | 6/10/2024 |