JWT na .NET Core 2.0

84

Byłem na niezłej przygodzie, aby JWT działał na DotNet core 2.0 (teraz osiągam ostateczne wydanie dzisiaj). Jest mnóstwo dokumentacji, ale wydaje się, że cały przykładowy kod korzysta z przestarzałych interfejsów API i pojawia się na nowo w Core. To naprawdę oszałamiające, gdy zastanawiasz się, jak dokładnie ma zostać wdrożony. Próbowałem użyć Jose, ale aplikacja. UseJwtBearerAuthentication zostało uznane za przestarzałe i nie ma dokumentacji na temat dalszych działań.

Czy ktoś ma projekt typu open source, który używa dotnet core 2.0, który może po prostu przeanalizować JWT z nagłówka autoryzacji i pozwolić mi na autoryzowanie żądań tokenu JWT zakodowanego HS256?

Poniższa klasa nie zgłasza żadnych wyjątków, ale żadne żądania nie są autoryzowane i nie mam żadnych wskazówek, dlaczego są nieautoryzowane. Odpowiedzi są puste 401, więc dla mnie oznacza to, że nie było wyjątku, ale sekret nie pasuje.

Dziwną rzeczą jest to, że moje tokeny są szyfrowane algorytmem HS256, ale nie widzę wskaźnika, który kazałby mu zmusić go do użycia tego algorytmu w dowolnym miejscu.

Oto klasa, którą mam do tej pory:

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
źródło
Wyłączyłem weryfikację podpisywania bez zmian, wszystkie żądania używają [Authorize] 401
Michael Draper
2
Czy możesz wysłać pełny kod? lub projekt demo, chciałbym zobaczyć, jak to działa ...
Piotr Stuliński
Jest jeszcze jeden post, który może ci pomóc. stackoverflow.com/a/48295906/8417618
Marco Barbero

Odpowiedzi:

88

Oto pełna działająca minimalna próbka z kontrolerem. Mam nadzieję, że możesz to sprawdzić za pomocą wywołania Postman lub JavaScript.

  1. appsettings.json, appsettings.Development.json. Dodaj sekcję. Uwaga, klucz powinien być dość długi, a wystawca to adres usługi:

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

    !!! W prawdziwym projekcie nie przechowuj klucza w pliku appsettings.json. Powinien być przechowywany w zmiennej środowiskowej i potraktować to w ten sposób:

    Environment.GetEnvironmentVariable("JWT_KEY");
    

AKTUALIZACJA : Widząc, jak działają ustawienia .net core, nie musisz pobierać ich dokładnie ze środowiska. Możesz użyć ustawienia. Jednak zamiast tego możemy zapisać tę zmienną do zmiennych środowiskowych w środowisku produkcyjnym, wtedy nasz kod będzie preferował zmienne środowiskowe zamiast konfiguracji.

  1. AuthRequest.cs: Dto przechowuje wartości do przekazywania loginu i hasła:

    public class AuthRequest
    {
        public string UserName { get; set; }
        public string Password { get; set; }
    }
    
  2. Startup.cs w metodzie Configure () PRZED app.UseMvc ():

    app.UseAuthentication();
    
  3. Startup.cs w 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. Dodaj kontroler:

        [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");
            }}
    

To wszystko ludzie! Twoje zdrowie!

AKTUALIZACJA: Ludzie pytają, jak uzyskać obecnego użytkownika. Do zrobienia:

  1. W Startup.cs w ConfigureServices () dodaj

    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    
  2. W kontrolerze dodaj do konstruktora:

    private readonly int _currentUser;
    public MyController(IHttpContextAccessor httpContextAccessor)
    {
       _currentUser = httpContextAccessor.CurrentUser();
    }
    
  3. Dodaj gdzieś rozszerzenie i używaj go w swoim kontrolerze (używając ...)

    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
źródło
1
To było dla mnie bardzo pomocne. Jedyne, czego wciąż nie wiem, to jak sprawdzić token dla kolejnych połączeń lub jak określić, kim jest aktualnie zalogowany użytkownik.
Travesty3
To naprawdę łatwe. W wywołaniu metody kontrolera: var currentUser = HttpContext.User.Identity.Name; Twoje zdrowie!
alerya
1
Koleś, to była ogromna pomoc, bardzo dziękuję za szczegółową odpowiedź!
Ryanman
1
Dzięki za następujące informacje. Uratowało mnie to przed błędem 401 Unauthorized. 3.Startup.cs w metodzie Configure () PRZED app.UseMvc (): app.UseAuthentication ();
Sumia,
1
To jest sprzeczność, nie powinniśmy przechowywać klucza w pliku appsettings, ale pobieramy go tutaj Konfiguracja ["Tokens: Key"]). Jeśli mamy inną usługę, która bezpiecznie pobiera klucz, w jaki sposób moglibyśmy uwzględnić to w ConfigureServices?
War Gravy
18

Moje tokenValidationParametersprace, gdy wyglądają tak:

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

i

    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";
    }

Ponadto dodaj opcje.RequireHttpsMetadata = false w ten sposób:

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

EDYCJA :

Nie zapomnij zadzwonić

 app.UseAuthentication();

w Startup.cs -> Configure method before app.UseMvc ();

Adrian Księżarczyk
źródło
Założę się, że to app.UseAuthentication (); zadzwoń, to załatwi sprawę, nie wiedziałem, że tego potrzebuję. Dziękuję Ci!
Michael Draper,
Myślę, że musisz również określić ValidAudience, ValidIssuer i IssuerSigningKey. Bez tego u mnie się nie udało
Adrian Księżarczyk
Tak, dokładnie to było. Musiałem dodać app.UseAuthentication () i to wszystko. Dziękuję Ci bardzo!
Michael Draper
3
+1 za app.UseAuthentication();notatkę wywołaną wcześniej, app.UseMvc();jeśli tego nie zrobisz, otrzymasz 401, nawet jeśli token zostanie pomyślnie autoryzowany - poświęciłem na to około 2 dni!
pcdev
1
"app.UseAuthentication ();" spędziłem cały dzień na naprawieniu problemu 401 po uaktualnieniu .net core z 1.0 do 2.0, ale nie znalazłem rozwiązania, dopóki nie zobaczyłem tego postu. Dzięki Adrian.
Chan,
8

Asp.net Core 2.0 JWT Bearer Token Authentication with Web Api Demo

Dodaj pakiet „ 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 Konfiguruj ()

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

User.cs // To jest przykładowa klasa. To może być wszystko.

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 // To tylko klasa kontekstu. To może być wszystko.

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();
    }
}

Test na PostMan: W odpowiedzi otrzymasz token.

Przekaż TokenType i AccessToken w nagłówku w innych usługach internetowych. wprowadź opis obrazu tutaj

Powodzenia! Jestem tylko początkującym. Spędziłem tylko tydzień, aby rozpocząć naukę asp.net core.

Abdul Hameed
źródło
Otrzymuję InvalidOperationException: nie można rozpoznać usługi typu „WebApplication8.UserContext” podczas próby aktywacji „AccountController”. kiedy próbuję zadzwonić do listonosza na Post to account / api / token
Kirsten Greed
7

Oto rozwiązanie dla Ciebie.

Najpierw w pliku startup.cs skonfiguruj go jako usługi:

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

po drugie, wywołaj te usługi w config

          app.UseAuthentication();

teraz możesz go użyć w swoim kontrolerze, dodając atrybut

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

Aby uzyskać szczegółowe informacje na temat kodu źródłowego używającego angular jako Frond-end, zobacz tutaj

Długie pole
źródło
To była odpowiedź, która uratowała mój bekon! Byłoby miło móc po prostu użyć [Autoryzuj]. Wyobraź sobie, że można sobie z tym poradzić za pomocą Startup.cs
Simon
1
Simon, ponieważ możesz mieć więcej niż jeden schemat w tej samej podstawowej aplikacji mvc asp.net, takiej jak services.AddAuthentication (). AddCookie (). AddJwtBearer ();
Long Field
1
możesz także ustawić domyślny schemat uwierzytelniania za pomocą services.AddAuthorizationfunkcji w uruchomieniu.
Neville Nazerane
Aby śledzić instrukcję @NevilleNazerane, kod do ustawiania domyślnego schematu uwierzytelniania (który będzie używany ze zwykłym dekoratorem [Autoryzuj]), kod jest odpowiedzią na to pytanie. To services.AddAuthentication (sharedOptions => {sharedOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; sharedOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;})
Ryanman
Jeśli spróbuję wykonać ten przykład dla IssuerSigningKey, pojawia się błąd Nie można przekonwertować typu źródła „string” na typ docelowy „Microsoft.IdentityModel.Tokens.SecurityKey”
Kirsten Greed
4

Oto moja implementacja dla interfejsu 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"
  },
  ...
}

Powyższy kod umożliwia autoryzację na wszystkich kontrolerach. Aby umożliwić anonimowy dostęp, możesz udekorować cały kontroler:

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

lub po prostu udekoruj metodę, aby umożliwić pojedynczy punkt końcowy:

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

Uwagi:

  • To moja pierwsza próba autoryzacji AD - jeśli coś jest nie tak, daj mi znać!

  • Audiencemusi być zgodny z identyfikatorem zasobu żądanym przez klienta. W naszym przypadku nasz klient (aplikacja internetowa Angular) został zarejestrowany osobno w usłudze Azure AD i użył swojego identyfikatora klienta, który zarejestrowaliśmy jako odbiorcę w interfejsie API

  • ClientIdjest nazywany identyfikatorem aplikacji w portalu Azure (dlaczego ??), identyfikatorem aplikacji rejestracji aplikacji dla interfejsu API.

  • TenantIdnazywa się Directory ID w Azure Portal (dlaczego ??) i znajduje się w Azure Active Directory> Properties

  • W przypadku wdrażania interfejsu API jako aplikacji internetowej hostowanej na platformie Azure, upewnij się, że ustawiono ustawienia aplikacji:

    na przykład. AzureAD: Audience / xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

pcdev
źródło
3

Aby zaktualizować doskonałą odpowiedź @alerya, musiałem zmodyfikować klasę pomocnika, aby wyglądała tak;

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

Wtedy mogłem uzyskać identyfikator użytkownika w mojej warstwie usług. Wiem, że w kontrolerze jest to łatwe, ale trudniejsze wyzwanie.

spankymac
źródło