Autenticación JWT para API web ASP.NET

264

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

Veo soporte para .NET Core y para 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 en que pueda usar la <authentication>configuración similar a la forma en que uso formularios / autenticación de Windows?

Amir Popovich
fuente

Respuestas:

611

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

Ahora, muchas cosas cambiaron en seguridad, especialmente JWT se está volviendo popular. Aquí trataré de explicar cómo usar JWT de la manera más simple y básica que pueda, para que no nos perdamos de la selva de OWIN, Oauth2, ASP.NET Identity ... :).

Si no conoce el token JWT, debe echar un vistazo a:

https://tools.ietf.org/html/rfc7519

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

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

Ejemplo:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ

Un token JWT tiene tres secciones:

  1. Encabezado: formato JSON que está codificado en Base64
  2. Reclamaciones: formato JSON que está codificado en Base64.
  3. Firma: Creado y firmado basado en Encabezado y Reclamaciones que está codificado en Base64.

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

ingrese la descripción de la imagen aquí

Técnicamente, JWT utiliza una firma que está firmada desde encabezados y notificaciones con algoritmo de seguridad especificado en los encabezados (ejemplo: HMACSHA256). Por lo tanto, se requiere que JWT se transfiera a través de HTTP si almacena información confidencial en las notificaciones.

Ahora, para usar 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 validar el token cuando llega la solicitud. Eso es.

De vuelta a la demostración, para mantener el token JWT ligero, solo almaceno usernamey expiration timeen JWT. Pero de esta manera, debe reconstruir una nueva identidad local (principal) para agregar más información como: roles ... si desea hacer la autorización de roles. Pero, si desea agregar más información a JWT, depende de usted: es muy flexible.

En lugar de usar el middleware OWIN, simplemente puede proporcionar un punto final de token JWT mediante la acción del 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;
    }
}

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

¿Cómo generar el token basado en username?

Puede usar 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á hecho. Ahora, ¿cómo validar el JWT cuando llega la solicitud? En la demostración que he creado, JwtAuthenticationAttributeque hereda de IAuthenticationFilter(más detalles sobre el filtro de autenticación aquí ).

Con este atributo, puede autenticar cualquier acción: solo tiene que poner este atributo en esa acción.

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

También puede usar el middleware OWIN o DelegateHander si desea validar todas las solicitudes entrantes para su WebAPI (no específico para el 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)
        return false;

    if (!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 es usar la biblioteca JWT (paquete NuGet arriba) 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 principal:

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 agregar más información para verificar la autorización de roles.

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

Puede usar 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 de la solicitud autorizada, por ejemplo:

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

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s

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

cuongle
fuente
55
Bien explicado por @Cuong Le pero me gusta agregar más: si está utilizando OWIN, verifique la autenticación UseJwtBearerAuthentication disponible en Microsoft.Owin.Security.Jwt, puede usar este middleware owin en el WebAPI para validar automáticamente cada solicitud entrante. use la clase de inicio owin para registrar el middleware
Jek
55
@AmirPopovich No necesita configurar el token en la respuesta, el token debe almacenarse en otro lugar del lado del cliente, para la web, puede colocarlo en el almacenamiento local, cada vez que envíe una solicitud HTTP, coloque el token en el encabezado.
cuongle
77
Wow, esta es la explicación más simple que he visto en mucho tiempo. +100 si pudiera
gyozo kudor
44
@Homam: Lo siento por esta respuesta tardía, la mejor manera de generar es: varhmac = new HMACSHA256();var key = Convert.ToBase64String(hmac.Key);
cuongle
44
Cualquiera que use el código de demostración del repositorio de CuongLe notará que hay un error en el que las solicitudes sin encabezado de autorización no se manejan, lo que significa que cualquier consulta sin una puede pasar (¡un punto final no tan seguro!). Hay una solicitud de extracción de @magicleon para solucionar este problema aquí: github.com/cuongle/WebApi.Jwt/pull/4
Chucky
11

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

Para eso utilizo el Startup.csarchivo OWIN y la Microsoft.Owin.Security.Jwtbiblioteca.

Para que la aplicación llegue Startup.cs, necesitamos modificar Web.config:

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

Así es como Startup.csdebe 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 primero, estaba tratando de implementar proveedores personalizados, etc. Pero no esperaba que fuera tan simple. OWINsolo rocas!

Solo una cosa para mencionar: después de habilitar la NSWagbiblioteca de inicio OWIN , dejó de funcionar para mí (por ejemplo, algunos de ustedes querrían generar automáticamente proxy HTTP de mecanografía para la aplicación Angular).

La solución también era muy simple - Me reemplazado NSWagcon Swashbuckley no tenía ninguna otra cuestión.


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 encabezados parte - "Authorization": "Bearer " + localStorage.getItem('token')

Alex Herman
fuente
I replaced NSWag with Swashbuckle and didn't have any further issues.¿Swashbuckle tiene la capacidad de generar archivos de mecanografía o es algo que usted mismo agregó?
aplastar
@crush swashbucle es una biblioteca de back-end que proporciona json, como la biblioteca nuget nswag solo mejor. Para producir un archivo mecanografiado, aún debe usar el paquete nswag de npm.
Alex Herman
Bien, ya tengo swashbuckle en mi proyecto por algún tiempo, sonaba como si estuvieras sugiriendo que podría generar los modelos TypeScript en lugar de nswag. No soy fanático de nswag ... es pesado. He creado mi propia conversión de C # -> TypeScript que está conectada a Swashbuckle - genera los archivos como un proceso posterior a la compilación y los publica en un feed npm para nuestros proyectos. Solo quería asegurarme de no haber pasado por alto un proyecto Swashbuckle que ya estaba haciendo lo mismo.
enamorado
8

Aquí hay 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 devuelve 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 ConfigureServicesdentro de su startup.cs para agregar autenticación JWT como su servicio de autenticación predeterminado como este:

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 como esta:

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

ALTERNATIVAMENTE , también puede (no es necesario) llenar todos sus reclamos de su base de datos, ya que esto solo se ejecutará una vez en el inicio de su 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 puede poner el filtro de Políticas en cualquiera de los métodos que desea autorizar 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
fuente
3

Creo que debería usar algún servidor de fiesta 3D para admitir el token JWT y no hay compatibilidad JWT lista para usar en WEB API 2.

Sin embargo, hay un proyecto OWIN para admitir algún formato de token firmado (no JWT). Funciona como un protocolo OAuth reducido para proporcionar solo una forma simple de autenticación para un sitio web.

Puede leer más sobre esto, por ejemplo, aquí .

Es bastante largo, pero la mayoría de las partes son detalles con controladores e identidad ASP.NET que puede que no necesite en absoluto. Lo más importante son

Paso 9: Agregue soporte para OAuth Bearer Tokens Generation

Paso 12: Prueba de la API de back-end

Allí puede leer cómo configurar un punto final (por ejemplo, "/ token") al que puede acceder desde la interfaz (y detalles sobre el formato de la solicitud).

Otros pasos proporcionan detalles sobre cómo conectar ese punto final a la base de datos, etc. y puede elegir las partes que necesita.

Ilya Chernomordik
fuente
2

En mi caso, el JWT es creado por una API separada, por lo que ASP.NET solo necesita decodificarlo y validarlo. A diferencia de la respuesta aceptada, estamos utilizando RSA, que es un algoritmo no simétrico, por lo que la SymmetricSecurityKeyclase mencionada anteriormente no funcionará.

Aquí está el resultado.

using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Threading;
using System.Threading.Tasks;

    public static async Task<JwtSecurityToken> VerifyAndDecodeJwt(string accessToken)
    {
        try
        {
            var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{securityApiOrigin}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
            var openIdConfig = await configurationManager.GetConfigurationAsync(CancellationToken.None);
            var validationParameters = new TokenValidationParameters()
            {
                ValidateLifetime = true,
                ValidateAudience = false,
                ValidateIssuer = false,
                RequireSignedTokens = true,
                IssuerSigningKeys = openIdConfig.SigningKeys,
            };
            new JwtSecurityTokenHandler().ValidateToken(accessToken, validationParameters, out var validToken);
            // threw on invalid, so...
            return validToken as JwtSecurityToken;
        }
        catch (Exception ex)
        {
            logger.Info(ex.Message);
            return null;
        }
    }
Ron Newcomb
fuente