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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DS.Foundation.Authentication" Version="8.4.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DS.Foundation.Authentication" Version="8.4.2" />
                    
Directory.Packages.props
<PackageReference Include="DS.Foundation.Authentication" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DS.Foundation.Authentication --version 8.4.2
                    
#r "nuget: DS.Foundation.Authentication, 8.4.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DS.Foundation.Authentication@8.4.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DS.Foundation.Authentication&version=8.4.2
                    
Install as a Cake Addin
#tool nuget:?package=DS.Foundation.Authentication&version=8.4.2
                    
Install as a Cake Tool

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 de ValidIssuers. 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 iss del token es exacta y distingue mayúsculas: https://Security.test no coincide con https://security.test, y https://security.test/ tampoco.
  • Hace falta al menos una de las dos claves. Si no queda ningún emisor, Validate() lanza InvalidOperationException: System:Authentication:Jwt:Issuer or System:Authentication:Jwt:ValidIssuers is required.
  • Emisor con el que se firma (JwtAuthenticationOptions.GetDefaultIssuer(), usado por CreateJwtFromToken(token)): Issuer si tiene valor; si no, la primera entrada válida de ValidIssuers, en el orden de la configuración. Para firmar con otro emisor aceptado use CreateJwtFromToken(token, issuer).
  • Una aplicación configurada solo con Issuer funciona 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 (sin Issuer) necesita 8.4.1 o posterior. Las versiones anteriores ignoran ValidIssuers, exigen Issuer y fallan al arrancar o al validar el primer token.

Cambiar el emisor de los tokens
  1. 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 en Issuer. Despliegue todas.
  2. 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 su Issuer.
  3. 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) y ClaimTypes.GroupSid (TenantCode).
  • Un ClaimTypes.Role por cada rol general (Roles). En el JWT, un rol queda como texto y varios como arreglo, por ejemplo ["admin","ventas"]. Así funcionan User.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 a ClaimTypes.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:

  • UserCode toma el valor de UserId como texto.
  • Roles acepta 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 ApplicationRoles con el mismo ApplicationCode (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

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) y ClaimTypes.GroupSid (TenantCode). Un valor null se escribe como texto vacío.
  • Un ClaimTypes.Role por 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 de ApplicationRoles, con el JSON de UserApplicationRole ({"A":"COM","R":["COMPRADOR"]}) y tipo de valor JsonClaimValueTypes.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 SecretKey en appsettings.json versionado ni en el código: use variables de entorno o un almacén de secretos.
  • No usar JsonWebTokenService.CreateTokenFromJwt: está obsoleto y lanza SecurityTokenValidationException en lugar de devolver false. Use TryValidateAndCreateUserToken.
  • 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 ApplicationRoles por su cuenta, ni repetir el literal "ApplicationRoles": use UserTokenMapper.ToClaims o ToClaimsPrincipal y, 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed