Авторизация API с помощью JWT токена в .NET 5.0

В этой статье мы узнаем, как создать и настроить аутентификацию JWT Bearer для авторизации API с помощью .NET 5.0. Есть много ресурсов, которые раскрывают тему «JWT Auth», но в этой статье мы будем сосредоточены на реализации пользовательской аутентификации JWT с кастомным промежуточным программным обеспечением и атрибутом авторизации.

JWT аутентификация

Схема JWT аутентификации

Необходимые инструменты:

Visual Studio 2019 - Можно загрузить здесь .Net 5.0 SDK - Можно загрузить здесь

План работы:

  1. Настройка проекта веб-API .NET 5.0.
  2. Настройка аутентификацию JWT
  3. Генерация токена JWT.
  4. Валидация токена JWT, используя кастомное промежуточное ПО и атрибут авторизации.
  5. Тестирование API с использованием Swagger.

Настройка проект .Net 5.0 Web API

Откройте Visual Studio, выберите «Создать новый проект» и нажмите кнопку «Далее».

Visual Studio

Добавьте «Имя проекта» и «Имя решения» также выберите путь, чтобы сохранить проект в этом месте, нажмите «Далее».

Visual Studio: создание проекта

Теперь выберите целевой фреймворк .NET 5.0, что мы получаем, как только мы устанавливаем SDK, а также получим еще один вариант для настройки поддержки OpenAPI по умолчанию с помощью этого флажка.

Visual Studio NET 5

Настройка JWT-аутентификации

Чтобы настроить JWT (JSON веб-токен), у нас должен быть установлен пакет Nuget внутри проекта, поэтому давайте сначала добавим зависимости проекта.

Пакеты NuGet для установки

Внутри Visual Studio - нажмите на Tools -> Nuget Package Manager -> Manage Nuget packages for solution.

Настройка JWT-аутентификации - установка необходимых nuget-пакетов

Установить через консоль.

Install-Package Microsoft.AspNetCore.Authentication.JwtBearer -Version 5.0.7

Первый шаг - настроить аутентификацию JWT в нашем проекте. Для этого нам нужно зарегистрировать схему JWT в Swagger Service, используя метод Addauthentication.

Давайте определим службу SWARGER и закрепим за ней авторизацию по JWT.

#region Swagger Configuration
            services.AddSwaggerGen(swagger =>
            {
                //This is to generate the Default UI of Swagger Documentation
                swagger.SwaggerDoc("v1", new OpenApiInfo
                {
                    Version = "v1",
                    Title = "JWT Token Authentication API",
                    Description = "ASP.NET Core 5.0 Web API"
                });
                // To Enable authorization using Swagger (JWT)
                swagger.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
                {
                    Name = "Authorization",
                    Type = SecuritySchemeType.ApiKey,
                    Scheme = "Bearer",
                    BearerFormat = "JWT",
                    In = ParameterLocation.Header,
                    Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 12345abcdef\"",
                });
                swagger.AddSecurityRequirement(new OpenApiSecurityRequirement
                {
                    {
                          new OpenApiSecurityScheme
                            {
                                Reference = new OpenApiReference
                                {
                                    Type = ReferenceType.SecurityScheme,
                                    Id = "Bearer"
                                }
                            },
                            new string[] {}
                    }
                });
            });
            #endregion

Добавим службу для выполнения аутентификации, а также вызовем AddJWTBearer для конфигурации авторизации.

#region Authentication
            services.AddAuthentication(option =>
            {
                option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

            }).AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = false,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = Configuration["Jwt:Issuer"],
                    ValidAudience = Configuration["Jwt:Audience"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"])) //Configuration["JwtToken:SecretKey"]
                };
            });
            #endregion

В приведенном выше примере мы указали параметры, которые необходимо учитывать для проверки токена.

  • Проверка на сервере (Validateissuer = true), который генерирует токен.
  • Проверка получателя токена, авторизован ли он для получения токена (ValidateAudience = True)
  • Проверка, не истек ли токен и валиден ли ключ подписания эмитента (ValidateLifetime = True)
  • Проверка подписи токена (ValidateissuerSigningKey = True)

Мы должны указать значения для Audience, Issuer и Secret key в этом проекте, мы сохранили эти значения внутри файла appsettings.json.

appsettings.json

"Jwt": {
    "Key": "SecretKey10125779374235322",
    "Issuer": "https://localhost:44341",
    "Audience": "http://localhost:4200"
},

Генерация JWT токена

Давайте создадим контроллер с именем AuthController внутри папки controllers, и добавим метод Auth, который отвечает для проверку учетных данных входа и создадим токен на основе имени пользователя. Мы отметили этот метод с помощью атрибута AllowAnonymous для обхода аутентификации. Этот метод ожидает объект LoginModel для имени пользователя и пароля.

Мы создали директорию Services, в которой находится наша основная бизнес-логика, и мы используем внедрение зависимостей для использования этих служб в контроллере.

В демонстрационных целях я жестко запрограммировал значения внутри метода для проверки модели.

UserService.cs

public bool IsValidUserInformation(LoginModel model)
{
     if (model.UserName.Equals("Jay") && model.Password.Equals("123456")) return true;
           else return false;
}

IUserService.cs

public interface IUserService
{
     bool IsValidUserInformation(LoginModel model);
}

Добавьте эту службу в класс Startup в методе ConfigureServices.

services.AddTransient<IUserService,UserService>();

Внутри AuthController создадим приватный метод, известный как GenerateJWTToken, чтобы создать токен на основе эмитента, потребителя и секретного ключа, которые мы определили в файле appSettings.json.

using JWTAuth_Validation.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.ComponentModel.DataAnnotations;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace JWTAuth_Validation.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AuthController : ControllerBase
    {
        private readonly IConfiguration _configuration;
        private readonly IUserService _userService;
        public AuthController(IConfiguration configuration,IUserService userService)
        {
            _configuration = configuration;
            _userService = userService;
        }
        [AllowAnonymous]
        [HttpPost(nameof(Auth))]
        public IActionResult Auth([FromBody] LoginModel data)
        {
            bool isValid = _userService.IsValidUserInformation(data);
            if (isValid)
            {
                var tokenString = GenerateJwtToken(data.UserName);
                return Ok(new { Token = tokenString, Message = "Success" });
            }
            return BadRequest("Please pass the valid Username and Password");
        }
        [Authorize(AuthenticationSchemes = Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults.AuthenticationScheme)]
        [HttpGet(nameof(GetResult))]
        public IActionResult GetResult()
        {
            return Ok("API Validated");
        }
        /// <summary>
        /// Generate JWT Token after successful login.
        /// </summary>
        /// <param name="accountId"></param>
        /// <returns></returns>
        private string GenerateJwtToken(string userName)
        {
            var tokenHandler = new JwtSecurityTokenHandler();
            var key = Encoding.ASCII.GetBytes(_configuration["Jwt:key"]);
            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = new ClaimsIdentity(new[] { new Claim("id", userName) }),
                Expires = DateTime.UtcNow.AddHours(1),
                Issuer = _configuration["Jwt:Issuer"],
                Audience = _configuration["Jwt:Audience"],
                SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
            };
            var token = tokenHandler.CreateToken(tokenDescriptor);
            return tokenHandler.WriteToken(token);
        }

    }
    #region JsonProperties
    /// <summary>
    /// Json Properties
    /// </summary>
    public class LoginModel
    {
        [Required]
        public string UserName { get; set; }
        [Required]
        public string Password { get; set; }
    }
    #endregion
}

Как только мы включили аутентификацию, я создал образец Get API, добавив атрибут авторизации, чтобы этот API выполнил проверку токена, передаваемого с помощью HTTP-запроса.

Если кто-то попытается получить доступ к этому API без надлежащего токена, он выдаст 401 Unauthorized Access в качестве ответа. Если мы хотим обойти аутентификацию для любого из наших существующих методов, мы можем пометить этот метод атрибутом AllowAnonymous.

Проверка JWT токена, используя middleware и атрибут Authorization

Ниже показано настраиваемое промежуточное ПО JWT, которое проверяет токен в заголовке запроса Authorization, если он существует. При успешной проверке промежуточное ПО извлекает этого связанного пользователя из базы данных и закрепляет его за своим контекстом. context.Items["User"] делает текущую учетную запись доступной для любого другого кода, работающего в текущей области запроса, который мы будем использовать ниже в настраиваемом атрибуте авторизации.

Создайте папку с именем Middleware, в которой добавьте классы JWTMiddleware и AuthorizeAttribute.

JWTMiddleware.cs

using JWTAuth_Validation.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace JWTAuth_Validation.Middleware
{
    public class JWTMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly IConfiguration _configuration;
        private readonly IUserService _userService;

        public JWTMiddleware(RequestDelegate next, IConfiguration configuration, IUserService userService)
        {
            _next = next;
            _configuration = configuration;
            _userService = userService;
        }

        public async Task Invoke(HttpContext context)
        {
            var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();

            if (token != null)
                attachAccountToContext(context, token);

            await _next(context);
        }

        private void attachAccountToContext(HttpContext context, string token)
        {
            try
            {
                var tokenHandler = new JwtSecurityTokenHandler();
                var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Key"]);
                tokenHandler.ValidateToken(token, new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(key),
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
                    ClockSkew = TimeSpan.Zero
                }, out SecurityToken validatedToken);

                var jwtToken = (JwtSecurityToken)validatedToken;
                var accountId = jwtToken.Claims.First(x => x.Type == "id").Value;

                // attach account to context on successful jwt validation
                context.Items["User"] = _userService.GetUserDetails();
            }
            catch
            {
                // do nothing if jwt validation fails
                // account is not attached to context so request won't have access to secure routes
            }
        }
    }
}

AuthorizeAttribute.cs

using JWTAuth_Validation.Controllers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System;

namespace JWTAuth_Validation.Middleware
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class AuthorizeAttribute : Attribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            var account = (LoginModel)context.HttpContext.Items["User"];
            if (account == null)
            {
                // not logged in
                context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized };
            }
        }
    }
}

Давайте внедрим это промежуточное ПО в класс Startup.

app.UseMiddleware<JWTMiddleware>();

Теперь каждый раз, когда кто то попадает в API с заголовком авторизации, промежуточное ПО сначала проверит токен и отправляет соответствующую информацию в ответ на запрос.

Тестирование API с помощью Swagger (OpenAPI)

Запустите приложение. Это приведет нас к странице Swagger Index со всей настроенной конфигурацией, которую мы сделали в проекте.

Тестирование API с помощью Swagger

Давайте передадим действительные учетные данные Auth API, чтобы получить токен доступа.

{
  "userName": "Jay",
  "password": "123456"
}

Тестирование API с JWT токеном

Скопируйте токен и добавьте тот же токен в форме авторизации, а затем нажмите Authorize.

Тестирование авторизации с помощью Swagger

Теперь всё API авторизовано в Swagger. Давайте проверим API Get.

Заключение Тестирование метода GET в API с помощью Swagger

JWT давно известен в веб-разработке. Это открытый стандарт, который позволяет безопасно и компактно передавать данные для авторизации в виде объекта JSON. В этой статье мы узнали, как создать и проверить JWT с помощью основного приложения ASP.NET.