Autenticación JWT para ASP.NET Web API

Resuelto Amir Popovich asked hace 8 años • 7 respuestas

Estoy intentando admitir el token de portador JWT (JSON Web Token) en mi aplicación API web y me estoy perdiendo.

Veo soporte para .NET Core y aplicaciones OWIN.
Actualmente estoy alojando mi aplicación en IIS.

¿Cómo puedo lograr este módulo de autenticación en mi aplicación? ¿Hay alguna forma de utilizar la <authentication>configuración similar a la forma en que uso los formularios/autenticación de Windows?

Amir Popovich avatar Oct 27 '16 16:10 Amir Popovich
Aceptado

Respondí esta pregunta: Cómo proteger una API web ASP.NET hace 4 años usando HMAC.

Ahora, muchas cosas han cambiado en materia de seguridad, especialmente porque JWT se está volviendo popular. En esta respuesta, intentaré explicar cómo usar JWT de la manera más simple y básica que pueda, para no perdernos en la jungla de OWIN, Oauth2, ASP.NET Identity, etc.

Si no conoce los tokens JWT, debe consultar:

https://www.rfc-editor.org/rfc/rfc7519

Básicamente, un token JWT se ve así:

<base64-encoded header>.<base64-encoded claims>.<base64-encoded signature>

Ejemplo:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA 5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ

Un token JWT tiene tres secciones:

  1. Encabezado: formato JSON codificado en Base64
  2. Reclamaciones: formato JSON codificado en Base64.
  3. Firma: creada y firmada según el encabezado y los reclamos codificados en Base64.

Si utiliza el sitio web jwt.io con el token anterior, puede decodificar el token y verlo como se muestra a continuación:

Una captura de pantalla de jwt.io con la fuente jwt sin formato y el JSON decodificado que representa

Técnicamente, JWT utiliza una firma que se firma a partir de encabezados y reclamos con un algoritmo de seguridad especificado en los encabezados (ejemplo: HMACSHA256). Por lo tanto, JWT debe transferirse a través de HTTP si almacena información confidencial en sus reclamos.

Ahora, para utilizar la autenticación JWT, realmente no necesita un middleware OWIN si tiene un sistema Web Api heredado. El concepto simple es cómo proporcionar el token JWT y cómo validarlo cuando llega la solicitud. Eso es todo.

En la demostración que creé (github) , para mantener el token JWT liviano, solo almaceno usernamey expiration time. Pero de esta manera, debe reconstruir una nueva identidad local (principal) para agregar más información como roles, si desea realizar autorización de roles, etc. Pero, si desea agregar más información a JWT, depende de usted: es muy flexible.

En lugar de utilizar middleware OWIN, simplemente puede proporcionar un punto final de token JWT mediante una acción de controlador:

public class TokenController : ApiController
{
    // This is naive endpoint for demo, it should use Basic authentication
    // to provide token or POST request
    [AllowAnonymous]
    public string Get(string username, string password)
    {
        if (CheckUser(username, password))
        {
            return JwtManager.GenerateToken(username);
        }

        throw new HttpResponseException(HttpStatusCode.Unauthorized);
    }

    public bool CheckUser(string username, string password)
    {
        // should check in the database
        return true;
    }
}

Ésta es una acción ingenua; en producción, debe utilizar una solicitud POST o un punto final de autenticación básica para proporcionar el token JWT.

¿Cómo generar el token en función de username?

Puede utilizar el paquete NuGet llamado System.IdentityModel.Tokens.Jwtdesde Microsoft para generar el token, o incluso otro paquete si lo desea. En la demostración, uso HMACSHA256con SymmetricKey:

/// <summary>
/// Use the below code to generate symmetric Secret Key
///     var hmac = new HMACSHA256();
///     var key = Convert.ToBase64String(hmac.Key);
/// </summary>
private const string Secret = "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw==";

public static string GenerateToken(string username, int expireMinutes = 20)
{
    var symmetricKey = Convert.FromBase64String(Secret);
    var tokenHandler = new JwtSecurityTokenHandler();

    var now = DateTime.UtcNow;
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Name, username)
        }),

        Expires = now.AddMinutes(Convert.ToInt32(expireMinutes)),
        
        SigningCredentials = new SigningCredentials(
            new SymmetricSecurityKey(symmetricKey), 
            SecurityAlgorithms.HmacSha256Signature)
    };

    var stoken = tokenHandler.CreateToken(tokenDescriptor);
    var token = tokenHandler.WriteToken(stoken);

    return token;
}

El punto final para proporcionar el token JWT está listo.

¿Cómo validar el JWT cuando llega la solicitud?

En la demostración , he creado JwtAuthenticationAttributeel que hereda IAuthenticationFilter(más detalles sobre el filtro de autenticación aquí ) .

Con este atributo puedes autenticar cualquier acción: sólo tienes que poner este atributo en esa acción.

public class ValueController : ApiController
{
    [JwtAuthentication]
    public string Get()
    {
        return "value";
    }
}

También puede utilizar el middleware OWIN o DelegateHander si desea validar todas las solicitudes entrantes para su WebAPI (no específicas del Controlador o la acción)

A continuación se muestra el método principal del filtro de autenticación:

private static bool ValidateToken(string token, out string username)
{
    username = null;

    var simplePrinciple = JwtManager.GetPrincipal(token);
    var identity = simplePrinciple.Identity as ClaimsIdentity;

    if (identity == null || !identity.IsAuthenticated)
        return false;

    var usernameClaim = identity.FindFirst(ClaimTypes.Name);
    username = usernameClaim?.Value;

    if (string.IsNullOrEmpty(username))
       return false;

    // More validate to check whether username exists in system

    return true;
}

protected Task<IPrincipal> AuthenticateJwtToken(string token)
{
    string username;

    if (ValidateToken(token, out username))
    {
        // based on username to get more information from database 
        // in order to build local identity
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, username)
            // Add more claims if needed: Roles, ...
        };

        var identity = new ClaimsIdentity(claims, "Jwt");
        IPrincipal user = new ClaimsPrincipal(identity);

        return Task.FromResult(user);
    }

    return Task.FromResult<IPrincipal>(null);
}

El flujo de trabajo consiste en utilizar la biblioteca JWT (paquete NuGet anterior) para validar el token JWT y luego regresar ClaimsPrincipal. Puede realizar más validaciones, como verificar si el usuario existe en su sistema y agregar otras validaciones personalizadas si lo desea.

El código para validar el token JWT y recuperar el capital:

public static ClaimsPrincipal GetPrincipal(string token)
{
    try
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var jwtToken = tokenHandler.ReadToken(token) as JwtSecurityToken;

        if (jwtToken == null)
            return null;

        var symmetricKey = Convert.FromBase64String(Secret);

        var validationParameters = new TokenValidationParameters()
        {
            RequireExpirationTime = true,
            ValidateIssuer = false,
            ValidateAudience = false,
            IssuerSigningKey = new SymmetricSecurityKey(symmetricKey)
        };

        SecurityToken securityToken;
        var principal = tokenHandler.ValidateToken(token, validationParameters, out securityToken);

        return principal;
    }
    catch (Exception)
    {
        //should write log
        return null;
    }
}

Si se valida el token JWT y se devuelve el principal, debe crear una nueva identidad local y poner más información en ella para verificar la autorización del rol.

Recuerde agregar config.Filters.Add(new AuthorizeAttribute());(autorización predeterminada) en el ámbito global para evitar cualquier solicitud anónima a sus recursos.

Puede utilizar Postman para probar la demostración :

Solicitar token (ingenuo como mencioné anteriormente, solo para demostración):

GET http://localhost:{port}/api/token?username=cuong&password=1

Coloque el token JWT en el encabezado para la solicitud autorizada, ejemplo:

GET http://localhost:{port}/api/value

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s

La demostración se puede encontrar aquí: https://github.com/cuongle/WebApi.Jwt

cuongle avatar Oct 27 '2016 12:10 cuongle

Logré lograrlo con el mínimo esfuerzo (tan simple como con ASP.NET Core).

Para eso utilizo Startup.csel archivo y Microsoft.Owin.Security.Jwtla biblioteca OWIN.

Para que la aplicación funcione Startup.csdebemos modificar Web.config:

<configuration>
  <appSettings>
    <add key="owin:AutomaticAppStartup" value="true" />
    ...

Así es como Startup.csdebería verse:

using MyApp.Helpers;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Jwt;
using Owin;

[assembly: OwinStartup(typeof(MyApp.App_Start.Startup))]

namespace MyApp.App_Start
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseJwtBearerAuthentication(
                new JwtBearerAuthenticationOptions
                {
                    AuthenticationMode = AuthenticationMode.Active,
                    TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidAudience = ConfigHelper.GetAudience(),
                        ValidIssuer = ConfigHelper.GetIssuer(),
                        IssuerSigningKey = ConfigHelper.GetSymmetricSecurityKey(),
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true
                    }
                });
        }
    }
}

Muchos de ustedes usan ASP.NET Core hoy en día, por lo que, como pueden ver, no difiere mucho de lo que tenemos allí.

Realmente me dejó perplejo al principio, estaba tratando de implementar proveedores personalizados, etc. Pero no esperaba que fuera tan simple. OWIN¡solo rocas!

Solo una cosa para mencionar: después de habilitar OWIN Startup, NSWagla biblioteca dejó de funcionar para mí (por ejemplo, es posible que algunos de ustedes quieran generar automáticamente servidores proxy HTTP mecanografiados para la aplicación Angular).

La solución también fue muy simple: lo reemplacé NSWagy Swashbuckleno tuve más problemas.


Ok, ahora compartiendo ConfigHelpercódigo:

public class ConfigHelper
{
    public static string GetIssuer()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["Issuer"];
        return result;
    }

    public static string GetAudience()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["Audience"];
        return result;
    }

    public static SigningCredentials GetSigningCredentials()
    {
        var result = new SigningCredentials(GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256);
        return result;
    }

    public static string GetSecurityKey()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["SecurityKey"];
        return result;
    }

    public static byte[] GetSymmetricSecurityKeyAsBytes()
    {
        var issuerSigningKey = GetSecurityKey();
        byte[] data = Encoding.UTF8.GetBytes(issuerSigningKey);
        return data;
    }

    public static SymmetricSecurityKey GetSymmetricSecurityKey()
    {
        byte[] data = GetSymmetricSecurityKeyAsBytes();
        var result = new SymmetricSecurityKey(data);
        return result;
    }

    public static string GetCorsOrigins()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["CorsOrigins"];
        return result;
    }
}

Otro aspecto importante: envié el token JWT a través del encabezado de Autorización , por lo que el código mecanografiado me busca de la siguiente manera:

(el siguiente código es generado por NSWag )

@Injectable()
export class TeamsServiceProxy {
    private http: HttpClient;
    private baseUrl: string;
    protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;

    constructor(@Inject(HttpClient) http: HttpClient, @Optional() @Inject(API_BASE_URL) baseUrl?: string) {
        this.http = http;
        this.baseUrl = baseUrl ? baseUrl : "https://localhost:44384";
    }

    add(input: TeamDto | null): Observable<boolean> {
        let url_ = this.baseUrl + "/api/Teams/Add";
        url_ = url_.replace(/[?&]$/, "");

        const content_ = JSON.stringify(input);

        let options_ : any = {
            body: content_,
            observe: "response",
            responseType: "blob",
            headers: new HttpHeaders({
                "Content-Type": "application/json", 
                "Accept": "application/json",
                "Authorization": "Bearer " + localStorage.getItem('token')
            })
        };

Ver parte de encabezados -"Authorization": "Bearer " + localStorage.getItem('token')

Alex Herman avatar Oct 15 '2018 18:10 Alex Herman

A continuación se muestra una implementación mínima y segura de una autenticación basada en notificaciones utilizando el token JWT en una API web ASP.NET Core.

En primer lugar, debe exponer un punto final que devuelva un token JWT con notificaciones asignadas a un usuario:

 /// <summary>
        /// Login provides API to verify user and returns authentication token.
        /// API Path:  api/account/login
        /// </summary>
        /// <param name="paramUser">Username and Password</param>
        /// <returns>{Token: [Token] }</returns>
        [HttpPost("login")]
        [AllowAnonymous]
        public async Task<IActionResult> Login([FromBody] UserRequestVM paramUser, CancellationToken ct)
        {

            var result = await UserApplication.PasswordSignInAsync(paramUser.Email, paramUser.Password, false, lockoutOnFailure: false);

            if (result.Succeeded)
            {
                UserRequestVM request = new UserRequestVM();
                request.Email = paramUser.Email;


                ApplicationUser UserDetails = await this.GetUserByEmail(request);
                List<ApplicationClaim> UserClaims = await this.ClaimApplication.GetListByUser(UserDetails);

                var Claims = new ClaimsIdentity(new Claim[]
                                {
                                    new Claim(JwtRegisteredClaimNames.Sub, paramUser.Email.ToString()),
                                    new Claim(UserId, UserDetails.UserId.ToString())
                                });


                //Adding UserClaims to JWT claims
                foreach (var item in UserClaims)
                {
                    Claims.AddClaim(new Claim(item.ClaimCode, string.Empty));
                }

                var tokenHandler = new JwtSecurityTokenHandler();
                  // this information will be retrived from you Configuration
                //I have injected Configuration provider service into my controller
                var encryptionkey = Configuration["Jwt:Encryptionkey"];
                var key = Encoding.ASCII.GetBytes(encryptionkey);
                var tokenDescriptor = new SecurityTokenDescriptor
                {
                    Issuer = Configuration["Jwt:Issuer"],
                    Subject = Claims,

                // this information will be retrived from you Configuration
                //I have injected Configuration provider service into my controller
                    Expires = DateTime.UtcNow.AddMinutes(Convert.ToDouble(Configuration["Jwt:ExpiryTimeInMinutes"])),

                    //algorithm to sign the token
                    SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)

                };

                var token = tokenHandler.CreateToken(tokenDescriptor);
                var tokenString = tokenHandler.WriteToken(token);

                return Ok(new
                {
                    token = tokenString
                });
            }

            return BadRequest("Wrong Username or password");
        }

ahora necesita agregar autenticación a sus servicios en ConfigureServicessu startup.cs para agregar la autenticación JWT como su servicio de autenticación predeterminado de esta manera:

services.AddAuthentication(x =>
            {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
             .AddJwtBearer(cfg =>
             {
                 cfg.RequireHttpsMetadata = false;
                 cfg.SaveToken = true;
                 cfg.TokenValidationParameters = new TokenValidationParameters()
                 {
                     //ValidateIssuerSigningKey = true,
                     IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:Encryptionkey"])),
                     ValidateAudience = false,
                     ValidateLifetime = true,
                     ValidIssuer = configuration["Jwt:Issuer"],
                     //ValidAudience = Configuration["Jwt:Audience"],
                     //IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Key"])),
                 };
             });

ahora puede agregar políticas a sus servicios de autorización de esta manera:

services.AddAuthorization(options =>
            {
                options.AddPolicy("YourPolicyNameHere",
                                policy => policy.RequireClaim("YourClaimNameHere"));
            });

ALTERNATIVAMENTE , también puede (no es necesario) completar todos sus reclamos desde su base de datos, ya que esto solo se ejecutará una vez al iniciar la aplicación y agregarlos a políticas como esta:

  services.AddAuthorization(async options =>
            {
                var ClaimList = await claimApplication.GetList(applicationClaim);
                foreach (var item in ClaimList)
                {                        
                    options.AddPolicy(item.ClaimCode, policy => policy.RequireClaim(item.ClaimCode));                       
                }
            });

ahora puedes poner el filtro de Política en cualquiera de los métodos que quieras que estén autorizados de esta manera:

 [HttpPost("update")]
        [Authorize(Policy = "ACC_UP")]
        public async Task<IActionResult> Update([FromBody] UserRequestVM requestVm, CancellationToken ct)
        {
//your logic goes here
}

Espero que esto ayude

Zeeshan Adil avatar Mar 28 '2019 15:03 Zeeshan Adil