JWT на .NET Core 2.0


83

Я мав неабияку пригоду, щоб JWT працював над DotNet core 2.0 (зараз він виходить у фінальному випуску сьогодні). Існує тонна документації, але все приклади код , як представляється, використовуючи застарілий API , і приходячи в свіжому Ядро, Це позитивно запаморочливим , щоб з'ясувати , як саме це повинно бути реалізовано. Я намагався використовувати Хосе, але додаток. UseJwtBearerAuthentication застаріло, і немає документації щодо подальших дій.

Хтось має проект з відкритим кодом, який використовує dotnet core 2.0, який може просто проаналізувати JWT із заголовка авторизації та дозволити мені авторизувати запити на кодований токен JWT, кодований HS256?

Клас нижче не створює жодних винятків, але жодні запити не авторизовані, і я не отримую вказівки, чому вони несанкціоновані. Відповіді порожні 401-х, тому для мене це вказує, що не було винятку, але що секрет не відповідає.

Дивне, що мої токени зашифровані за допомогою алгоритму HS256, але я не бачу жодного індикатора, який би сказав йому, щоб змусити його використовувати де-небудь цей алгоритм.

Ось клас, який я маю на даний момент:

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

Я вимкнув перевірку підписання без змін, усі запити з використанням [Авторизувати] 401
Майкл Дрейпер

2
Не могли б ви опублікувати повний код? або демонстраційний проект, я хотів би побачити, як ви
змусили


Є ще одна публікація, яка може вам допомогти. stackoverflow.com/a/48295906/8417618
Марко Барберо

Відповіді:


87

Ось повний робочий мінімальний зразок з контролером. Сподіваюся, ви зможете перевірити це за допомогою виклику поштарки або JavaScript.

  1. appsettings.json, appsettings.Development.json. Додайте розділ. Зверніть увагу, ключ повинен бути досить довгим, а видавець - це адреса послуги:

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

    !!! У реальному проекті не зберігайте ключ у файлі appsettings.json. Його слід зберігати в змінній середовища та приймати так:

    Environment.GetEnvironmentVariable("JWT_KEY");
    

ОНОВЛЕННЯ : Побачивши, як працюють основні налаштування .net, вам не потрібно точно брати їх із середовища. Ви можете використовувати налаштування. Однак замість цього ми можемо записати цю змінну до змінних середовища у виробництві, тоді наш код віддасть перевагу змінним середовища замість конфігурації.

  1. AuthRequest.cs: D, щоб зберегти значення для передачі логіна та пароля:

    public class AuthRequest
    {
        public string UserName { get; set; }
        public string Password { get; set; }
    }
    
  2. Startup.cs у методі Configure () ДО app.UseMvc ():

    app.UseAuthentication();
    
  3. Startup.cs у 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. Додайте контролер:

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

Це все, шановні! На здоров’я!

ОНОВЛЕННЯ: Люди запитують, як отримати Поточного користувача. Робити:

  1. У Startup.cs в ConfigureServices () додати

    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    
  2. У контролері додайте до конструктора:

    private readonly int _currentUser;
    public MyController(IHttpContextAccessor httpContextAccessor)
    {
       _currentUser = httpContextAccessor.CurrentUser();
    }
    
  3. Додайте десь розширення та використовуйте його у своєму контролері (використовуючи ....)

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

1
Це мені дуже допомогло. Єдине, що мені досі незрозуміло, це те, як перевірити маркер для подальших дзвінків або як визначити, хто в даний момент зареєстрований користувач.
Травесті

Це дуже просто. У виклику методу контролера: var currentUser = HttpContext.User.Identity.Name; Ура!
alerya

1
Чувак, це було такою величезною допомогою, велике спасибі за детальну відповідь!
Райанман

1
Дякуємо за наступну інформацію. Це врятувало мене від несанкціонованої помилки 401. 3.Startup.cs у методі Configure () ПЕРЕД app.UseMvc (): app.UseAuthentication ();
Сумія

1
Це суперечливість, ми не повинні зберігати ключ у файлі налаштувань програми, поки ми отримуємо його тут. Якщо у нас є інша служба, яка надійно отримує ключ, як би ми включили це в ConfigureServices?
War Gravy

18

Мої tokenValidationParametersроботи, коли вони виглядають так:

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

і

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

Більше того, додайте параметри.RequireHttpsMetadata = false, як це:

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

РЕДАГУВАТИ :

Не забудьте зателефонувати

 app.UseAuthentication();

у Startup.cs -> Налаштувати метод перед app.UseMvc ();


Б'юся об заклад, що це додаток.UseAuthentication (); дзвінок, який зробить трюк, я не знав, що мені це потрібно. Дякую!
Michael Draper

Я думаю, вам також потрібно вказати ValidAudience, ValidIssuer та IssuerSigningKey. Без цього у мене не
вийшло

Так, саме це було. Мені потрібно було додати app.UseAuthentication (), і це все, що потрібно. Дуже дякую!
Michael Draper

3
+1 за дзвінок до app.UseAuthentication();нотатки, app.UseMvc();якщо цього не зробити, ви отримаєте 401, навіть коли маркер успішно авторизований - я витратив близько 2 днів, працюючи над цим!
pcdev

1
"app.UseAuthentication ();", я витратив цілий день, щоб виправити проблему 401 після оновлення .net core з 1.0 до 2.0, але не знайшов рішення, поки не побачу цю публікацію. Дякую Адріане.
Чан

8

Реалізація аутентифікації токенів Asp.net Core 2.0 JWT Bearer з демо Api

Додати пакет " 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 Configure ()

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

User.cs // Це клас моделі лише для прикладу. Це може бути що завгодно.

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 // Це просто контекстний клас. Це може бути що завгодно.

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

Тест на PostMan: Ви отримаєте маркер у відповідь.

Передайте TokenType та AccessToken у заголовку в інших веб-службах. введіть тут опис зображення

Удачі! Я просто новачок. Я витратив лише один тиждень, щоб почати вивчати ядро ​​asp.net.


Я отримую InvalidOperationException: Не вдається вирішити службу типу 'WebApplication8.UserContext' під час спроби активувати 'AccountController'. коли я намагаюся зателефонувати листоноші на Post to account / api / token
Кірстен Жадібна

7

Ось рішення для вас.

По-перше, у вашому startup.cs налаштуйте його як сервіси:

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

по-друге, зателефонуйте цим службам у конфігурації

          app.UseAuthentication();

тепер ви можете використовувати його у своєму контролері, додаючи атрибут

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

Для отримання детальної інформації про вихідний код, який використовує angular як Frond-end, дивіться тут


Це була відповідь, яка врятувала мій бекон! Було б непогано просто мати можливість використовувати [Авторизувати]. Уявіть, це можна впоратись з Startup.cs
Саймон

1
Саймоне, тому що ви можете мати більше однієї схеми в одному і тому ж основному додатку mvc asp.net, наприклад, services.AddAuthentication (). AddCookie (). AddJwtBearer ();
Long Field

1
Ви також можете встановити схему автентифікації за замовчуванням за допомогою services.AddAuthorizationфункції під час запуску.
Neville Nazerane

Щоб продовжити заяву @NevilleNazerane, кодом для встановлення схеми автентифікації за замовчуванням (яка буде використовуватися із звичайним [Authorize] Decorator) кодом відповідає на це питання. Це послуги.AddAuthentication (sharedOptions => {sharedOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; sharedOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;})
Ryanman

Якщо я спробую наслідувати цей приклад для IssuerSigningKey, я отримую повідомлення про помилку Не вдається перетворити джерело 'string' у цільовий тип 'Microsoft.IdentityModel.Tokens.SecurityKey'
Кірстен Greed

4

Ось моя реалізація для 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"
  },
  ...
}

Наведений вище код дозволяє авторизацію на всіх контролерах. Щоб дозволити анонімний доступ, ви можете прикрасити цілий контролер:

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

або просто прикрасьте метод, щоб дозволити одну кінцеву точку:

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

Примітки:

  • Це моя перша спроба автентифікації AD - якщо щось не так, будь ласка, дайте мені знати!

  • Audienceповинен відповідати ідентифікатору ресурсу, який вимагає клієнт. У нашому випадку наш клієнт (веб-програма Angular) був зареєстрований окремо в Azure AD, і він використовував свій ідентифікатор клієнта, який ми зареєстрували як аудиторію в API

  • ClientIdназивається ідентифікатором програми на порталі Azure (чому ??), ідентифікатором програми реєстрації програми для API.

  • TenantIdназивається ідентифікатором каталогу на порталі Azure (чому ??), який знаходиться в розділі Azure Active Directory> Властивості

  • Якщо ви розгортаєте API як веб-програму, розміщену в Azure, переконайтеся, що ви встановили параметри програми:

    напр. AzureAD: Аудиторія / xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx


3

Просто для оновлення чудової відповіді @alerya мені довелося змінити допоміжний клас, щоб виглядати так;

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

Тоді я міг отримати userId у своєму сервісному рівні. Я знаю, що в контролері це легко, але виклик ще нижче.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.