JWT en .NET Core 2.0

83

He estado en una gran aventura para que JWT funcione en DotNet core 2.0 (que ahora llega a la versión final hoy). Hay un montón de documentación, pero todo el código de ejemplo parece ser el uso de las API en desuso y que viene en fresco a Core, es vertiginoso positivamente a averiguar cómo exactamente se supone que debe ser implementado. Intenté usar Jose, pero app. UseJwtBearerAuthentication ha quedado obsoleto y no hay documentación sobre qué hacer a continuación.

¿Alguien tiene un proyecto de código abierto que use dotnet core 2.0 que simplemente pueda analizar un JWT del encabezado de autorización y permitirme autorizar solicitudes para un token JWT codificado en HS256?

La clase a continuación no arroja ninguna excepción, pero no se autorizan solicitudes y no obtengo ninguna indicación de por qué no están autorizadas. Las respuestas son 401 vacías, por lo que para mí eso indica que no hubo una excepción, pero que el secreto no coincide.

Una cosa extraña es que mis tokens están encriptados con el algoritmo HS256, pero no veo ningún indicador que le diga que lo obligue a usar ese algoritmo en cualquier lugar.

Aquí está la clase que tengo hasta ahora:

using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json.Linq;
using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace Site.Authorization
{
    public static class SiteAuthorizationExtensions
    {
        public static IServiceCollection AddSiteAuthorization(this IServiceCollection services)
        {
            var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("SECRET_KEY"));

            var tokenValidationParameters = new TokenValidationParameters
            {
                // The signing key must match!
                ValidateIssuerSigningKey = true,
                ValidateAudience = false,
                ValidateIssuer = false,
                IssuerSigningKeys = new List<SecurityKey>{ signingKey },


                // Validate the token expiry
                ValidateLifetime = true,
            };

            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;


            })

            .AddJwtBearer(o =>
            {
                o.IncludeErrorDetails = true;
                o.TokenValidationParameters  = tokenValidationParameters;
                o.Events = new JwtBearerEvents()
                {
                    OnAuthenticationFailed = c =>
                    {
                        c.NoResult();

                        c.Response.StatusCode = 401;
                        c.Response.ContentType = "text/plain";

                        return c.Response.WriteAsync(c.Exception.ToString());
                    }

                };
            });

            return services;
        }
    }
}
Michael Draper
fuente
Desactivé la validación de firmas sin cambios, todas las solicitudes utilizan [Autorizar] 401
Michael Draper
2
¿Podrías publicar el código completo? o un proyecto de demostración, me encantaría ver cómo conseguiste que esto funcionara ...
Piotr Stulinski
Hay otra publicación que puede ayudarte. stackoverflow.com/a/48295906/8417618
Marco Barbero

Respuestas:

87

Aquí hay una muestra mínima de trabajo completa con un controlador. Espero que puedas comprobarlo usando Postman o una llamada JavaScript.

  1. appsettings.json, appsettings.Development.json. Agrega una sección. Tenga en cuenta que la clave debe ser bastante larga y el emisor es una dirección del servicio:

    ...
    ,"Tokens": {
        "Key": "Rather_very_long_key",
        "Issuer": "http://localhost:56268/"
    }
    ...
    

    !!! En un proyecto real, no guarde la clave en el archivo appsettings.json. Debe mantenerse en la variable de entorno y tomarlo así:

    Environment.GetEnvironmentVariable("JWT_KEY");
    

ACTUALIZACIÓN : Al ver cómo funciona la configuración principal de .net, no es necesario que la extraiga exactamente del entorno. Puede utilizar la configuración. Sin embargo, en su lugar, podemos escribir esta variable en las variables de entorno en producción, entonces nuestro código preferirá las variables de entorno en lugar de la configuración.

  1. AuthRequest.cs: Dto mantener los valores para pasar el nombre de usuario y la contraseña:

    public class AuthRequest
    {
        public string UserName { get; set; }
        public string Password { get; set; }
    }
    
  2. Startup.cs en el método Configure () ANTES de app.UseMvc ():

    app.UseAuthentication();
    
  3. Startup.cs en ConfigureServices ():

    services.AddAuthentication()
        .AddJwtBearer(cfg =>
        {
            cfg.RequireHttpsMetadata = false;
            cfg.SaveToken = true;
    
            cfg.TokenValidationParameters = new TokenValidationParameters()
            {
                ValidIssuer = Configuration["Tokens:Issuer"],
                ValidAudience = Configuration["Tokens:Issuer"],
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"]))
            };
    
        });
    
  4. Agregar un controlador:

        [Route("api/[controller]")]
        public class TokenController : Controller
        {
            private readonly IConfiguration _config;
            private readonly IUserManager _userManager;
    
            public TokenController(IConfiguration configuration, IUserManager userManager)
            {
                _config = configuration;
                _userManager = userManager;
            }
    
            [HttpPost("")]
            [AllowAnonymous]
            public IActionResult Login([FromBody] AuthRequest authUserRequest)
            {
                var user = _userManager.FindByEmail(model.UserName);
    
                if (user != null)
                {
                    var checkPwd = _signInManager.CheckPasswordSignIn(user, model.authUserRequest);
                    if (checkPwd)
                    {
                        var claims = new[]
                        {
                            new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
                            new Claim(JwtRegisteredClaimNames.Jti, user.Id.ToString()),
                        };
    
                        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Tokens:Key"]));
                        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
    
                        var token = new JwtSecurityToken(_config["Tokens:Issuer"],
                        _config["Tokens:Issuer"],
                        claims,
                        expires: DateTime.Now.AddMinutes(30),
                        signingCredentials: creds);
    
                        return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) });
                    }
                }
    
                return BadRequest("Could not create token");
            }}
    

¡Eso es todo amigos! ¡Salud!

ACTUALIZACIÓN: La gente pregunta cómo obtener Usuario actual. Que hacer:

  1. En Startup.cs en ConfigureServices () agregue

    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    
  2. En un controlador, agregue al constructor:

    private readonly int _currentUser;
    public MyController(IHttpContextAccessor httpContextAccessor)
    {
       _currentUser = httpContextAccessor.CurrentUser();
    }
    
  3. Agregue en algún lugar una extensión y úsela en su controlador (usando ....)

    public static class IHttpContextAccessorExtension
    {
        public static int CurrentUser(this IHttpContextAccessor httpContextAccessor)
        {
            var stringId = httpContextAccessor?.HttpContext?.User?.FindFirst(JwtRegisteredClaimNames.Jti)?.Value;
            int.TryParse(stringId ?? "0", out int userId);
    
            return userId;
        }
    }
    
alerya
fuente
1
Esto fue muy útil para mí. Lo único que aún no tengo claro es cómo verificar el token para llamadas posteriores, o cómo determinar quién es el usuario que ha iniciado sesión actualmente.
Travesty3
Es muy facil. En la llamada al método del controlador: var currentUser = HttpContext.User.Identity.Name; ¡Salud!
alerya
1
Amigo, esto fue de gran ayuda, ¡muchas gracias por la respuesta detallada!
Ryanman
1
Gracias por la siguiente información. Me salvó del error 401 no autorizado. 3.Startup.cs en el método Configure () ANTES de app.UseMvc (): app.UseAuthentication ();
Sumia
1
Esto es una contradicción, no deberíamos almacenar la clave en el archivo de configuración de la aplicación, pero la estamos recuperando aquí Configuración ["Tokens: Key"]). Si tenemos un servicio diferente que recupera la clave de forma segura, ¿cómo lo incorporamos en ConfigureServices?
War Gravy
18

Mis tokenValidationParametersobras cuando se ven así:

 var tokenValidationParameters = new TokenValidationParameters
  {
      ValidateIssuerSigningKey = true,
      IssuerSigningKey = GetSignInKey(),
      ValidateIssuer = true,
      ValidIssuer = GetIssuer(),
      ValidateAudience = true,
      ValidAudience = GetAudience(),
      ValidateLifetime = true,
      ClockSkew = TimeSpan.Zero
   };

y

    static private SymmetricSecurityKey GetSignInKey()
    {
        const string secretKey = "very_long_very_secret_secret";
        var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));

        return signingKey;
    }

    static private string GetIssuer()
    {
        return "issuer";
    }

    static private string GetAudience()
    {
        return "audience";
    }

Además, agregue options.RequireHttpsMetadata = false así:

         .AddJwtBearer(options =>
       {         
           options.TokenValidationParameters =tokenValidationParameters         
           options.RequireHttpsMetadata = false;
       });

EDITAR :

No olvides llamar

 app.UseAuthentication();

en Startup.cs -> Configurar método antes de app.UseMvc ();

Adrian Księżarczyk
fuente
Apuesto a que es la app.UseAuthentication (); llamada que hará el truco, no sabía que lo necesitaba. ¡Gracias!
Michael Draper
Creo que también debe especificar ValidAudience, ValidIssuer y IssuerSigningKey. No funcionó sin él para mí
Adrian Księżarczyk
Sí, eso era exactamente lo que era. Necesitaba agregar app.UseAuthentication () y eso es todo lo que necesité. ¡Muchas gracias!
Michael Draper
3
+1 por la app.UseAuthentication();nota que se ha llamado antes, app.UseMvc();si no lo hace, obtendrá 401 incluso cuando el token se haya autorizado con éxito; ¡pasé aproximadamente 2 días trabajando en eso!
pcdev
1
"app.UseAuthentication ();", pasé un día entero para solucionar el problema 401 después de actualizar .net core de 1.0 a 2.0, pero no encontré la solución hasta que vi esta publicación. Gracias Adrian.
Chan
8

Implementación de autenticación de token de portador JWT de Asp.net Core 2.0 con demostración de API web

Agregar paquete " Microsoft.AspNetCore.Authentication.JwtBearer "

Startup.cs ConfigureServices ()

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(cfg =>
            {
                cfg.RequireHttpsMetadata = false;
                cfg.SaveToken = true;

                cfg.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidIssuer = "me",
                    ValidAudience = "you",
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("rlyaKithdrYVl6Z80ODU350md")) //Secret
                };

            });

Startup.cs Configurar ()

// ===== Use Authentication ======
        app.UseAuthentication();

User.cs // Es una clase modelo solo por ejemplo. Puede ser cualquier cosa.

public class User
{
    public Int32 Id { get; set; }
    public string Username { get; set; }
    public string Country { get; set; }
    public string Password { get; set; }
}

UserContext.cs // Es solo una clase de contexto. Puede ser cualquier cosa.

public class UserContext : DbContext
{
    public UserContext(DbContextOptions<UserContext> options) : base(options)
    {
        this.Database.EnsureCreated();
    }

    public DbSet<User> Users { get; set; }
}

AccountController.cs

[Route("[controller]")]
public class AccountController : Controller
{

    private readonly UserContext _context;

    public AccountController(UserContext context)
    {
        _context = context;
    }

    [AllowAnonymous]
    [Route("api/token")]
    [HttpPost]
    public async Task<IActionResult> Token([FromBody]User user)
    {
        if (!ModelState.IsValid) return BadRequest("Token failed to generate");
        var userIdentified = _context.Users.FirstOrDefault(u => u.Username == user.Username);
            if (userIdentified == null)
            {
                return Unauthorized();
            }
            user = userIdentified;

        //Add Claims
        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.UniqueName, "data"),
            new Claim(JwtRegisteredClaimNames.Sub, "data"),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        };

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("rlyaKithdrYVl6Z80ODU350md")); //Secret
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken("me",
            "you",
            claims,
            expires: DateTime.Now.AddMinutes(30),
            signingCredentials: creds);

        return Ok(new
        {
            access_token = new JwtSecurityTokenHandler().WriteToken(token),
            expires_in = DateTime.Now.AddMinutes(30),
            token_type = "bearer"
        });
    }
}

UserController.cs

[Authorize]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
    private readonly UserContext _context;

    public UserController(UserContext context)
    {
        _context = context;
        if(_context.Users.Count() == 0 )
        {
            _context.Users.Add(new User { Id = 0, Username = "Abdul Hameed Abdul Sattar", Country = "Indian", Password = "123456" });
            _context.SaveChanges();
        }
    }

    [HttpGet("[action]")]
    public IEnumerable<User> GetList()
    {
        return _context.Users.ToList();
    }

    [HttpGet("[action]/{id}", Name = "GetUser")]
    public IActionResult GetById(long id)
    {
        var user = _context.Users.FirstOrDefault(u => u.Id == id);
        if(user == null)
        {
            return NotFound();
        }
        return new ObjectResult(user);
    }


    [HttpPost("[action]")]
    public IActionResult Create([FromBody] User user)
    {
        if(user == null)
        {
            return BadRequest();
        }

        _context.Users.Add(user);
        _context.SaveChanges();

        return CreatedAtRoute("GetUser", new { id = user.Id }, user);

    }

    [HttpPut("[action]/{id}")]
    public IActionResult Update(long id, [FromBody] User user)
    {
        if (user == null)
        {
            return BadRequest();
        }

        var userIdentified = _context.Users.FirstOrDefault(u => u.Id == id);
        if (userIdentified == null)
        {
            return NotFound();
        }

        userIdentified.Country = user.Country;
        userIdentified.Username = user.Username;

        _context.Users.Update(userIdentified);
        _context.SaveChanges();
        return new NoContentResult();
    }


    [HttpDelete("[action]/{id}")]
    public IActionResult Delete(long id)
    {
        var user = _context.Users.FirstOrDefault(u => u.Id == id);
        if (user == null)
        {
            return NotFound();
        }

        _context.Users.Remove(user);
        _context.SaveChanges();

        return new NoContentResult();
    }
}

Prueba en PostMan: Recibirás un token en respuesta.

Pase TokenType y AccessToken en el encabezado en otros servicios web. ingrese la descripción de la imagen aquí

¡La mejor de las suertes! Solo soy un principiante. Solo pasé una semana para comenzar a aprender el núcleo de asp.net.

Abdul Hameed
fuente
Recibo InvalidOperationException: no se puede resolver el servicio para el tipo 'WebApplication8.UserContext' al intentar activar 'AccountController'. cuando intento llamar al cartero para publicar en la cuenta / api / token
Kirsten Greed
7

Aquí tienes una solución.

En su startup.cs, en primer lugar, configúrelo como servicios:

  services.AddAuthentication().AddJwtBearer(cfg =>
        {
            cfg.RequireHttpsMetadata = false;
            cfg.SaveToken = true;
            cfg.TokenValidationParameters = new TokenValidationParameters()
            {
                IssuerSigningKey = "somethong",
                ValidAudience = "something",
                :
            };
        });

segundo, llame a estos servicios en la configuración

          app.UseAuthentication();

ahora puedes usarlo en tu controlador agregando atributo

          [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
          [HttpGet]
          public IActionResult GetUserInfo()
          {

Para obtener todos los detalles del código fuente que utiliza angular como Frond-end, consulte aquí

Campo largo
fuente
¡Esta fue la respuesta que me salvó el tocino! Sería bueno poder usar [Autorizar]. Imagina que esto se puede manejar con Startup.cs
Simon
1
Simon, porque puede tener más de un esquema en la misma aplicación asp.net core mvc, como services.AddAuthentication (). AddCookie (). AddJwtBearer ();
Long Field
1
también puede establecer un esquema de autenticación predeterminado mediante la services.AddAuthorizationfunción en el inicio.
Neville Nazerane
Para dar seguimiento a la declaración de @NevilleNazerane, el código para establecer un esquema de autenticación predeterminado (que se usará con un decorador [Autorizar] simple), el código es la respuesta a esta pregunta. Es services.AddAuthentication (sharedOptions => {sharedOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; sharedOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;})
Ryanman,
Si trato de seguir este ejemplo para IssuerSigningKey, aparece un error No se puede convertir el tipo de fuente 'cadena' al tipo de destino 'Microsoft.IdentityModel.Tokens.SecurityKey'
Kirsten Greed
4

Aquí está mi implementación para una API .Net Core 2.0:

    public IConfigurationRoot Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // Add framework services
        services.AddMvc(
        config =>
        {
            // This enables the AuthorizeFilter on all endpoints
            var policy = new AuthorizationPolicyBuilder()
                                .RequireAuthenticatedUser()
                                .Build();
            config.Filters.Add(new AuthorizeFilter(policy));
            
        }
        ).AddJsonOptions(opt =>
        {
            opt.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
        });

        services.AddLogging();

        services.AddAuthentication(sharedOptions =>
        {
            sharedOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            sharedOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(options =>
        {
            options.Audience = Configuration["AzureAD:Audience"];  
            options.Authority = Configuration["AzureAD:AADInstance"] + Configuration["AzureAD:TenantId"];
        });            
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        app.UseAuthentication(); // THIS METHOD MUST COME BEFORE UseMvc...() !!
        app.UseMvcWithDefaultRoute();            
    }

appsettings.json:

{
  "AzureAD": {
    "AADInstance": "https://login.microsoftonline.com/",
    "Audience": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "ClientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "Domain": "mydomain.com",
    "TenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  },
  ...
}

El código anterior habilita la autenticación en todos los controladores. Para permitir el acceso anónimo, puede decorar un controlador completo:

[Route("api/[controller]")]
[AllowAnonymous]
public class AnonymousController : Controller
{
    ...
}

o simplemente decorar un método para permitir un único punto final:

    [AllowAnonymous]
    [HttpPost("anonymousmethod")]
    public async Task<IActionResult> MyAnonymousMethod()
    {
        ...
    }

Notas:

  • Este es mi primer intento de autenticación de AD. Si hay algún problema, ¡avíseme!

  • Audiencedebe coincidir con el ID de recurso solicitado por el cliente. En nuestro caso, nuestro cliente (una aplicación web Angular) se registró por separado en Azure AD y usó su Id. De cliente, que registramos como Audience en la API.

  • ClientIdse denomina Id. de aplicación en Azure Portal (¿por qué?), el Id. de aplicación del registro de la aplicación para la API.

  • TenantIdse llama Id. de directorio en Azure Portal (¿por qué?), se encuentra en Azure Active Directory> Propiedades.

  • Si implementa la API como una aplicación web alojada en Azure, asegúrese de establecer la configuración de la aplicación:

    p.ej. AzureAD: Audiencia / xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

pcdev
fuente
3

Solo para actualizar la excelente respuesta de @alerya, tuve que modificar la clase de ayuda para que se vea así;

public static class IHttpContextAccessorExtension
    {
        public static string CurrentUser(this IHttpContextAccessor httpContextAccessor)
        {           
            var userId = httpContextAccessor?.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; 
            return userId;
        }
    }

Entonces pude obtener el ID de usuario en mi capa de servicio. Sé que es fácil en el controlador, pero un desafío más adelante.

spankymac
fuente