JWT-аутентификация для ASP.NET Web API

Я пытаюсь поддерживать токен-носитель JWT (JSON Web Token) в своем приложении веб-API, и я теряюсь.

Я вижу поддержку .NET Core и приложений OWIN.
В настоящее время я размещаю свое приложение в IIS.

Как я могу добиться этого модуля аутентификации в моем приложении? Можно ли каким-либо образом использовать конфигурацию <authentication> аналогичную использованию форм/аутентификации Windows?

Ответ 1

Я ответил на этот вопрос: как обеспечить безопасность ASP.NET Web API 4 года назад с помощью HMAC.

Сейчас многое изменилось в сфере безопасности, особенно JWT становится популярным. Здесь я попытаюсь объяснить, как использовать JWT самым простым и простым способом, каким только могу, чтобы мы не потерялись в джунглях OWIN, Oauth2, ASP.NET Identity... :).

Если вы не знаете токен JWT, вам нужно немного взглянуть на:

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

По сути, токен JWT выглядит так:

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

Пример:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ

Токен JWT состоит из трех разделов:

  1. Заголовок: формат JSON, который закодирован в Base64
  2. Заявки: формат JSON, закодированный в Base64.
  3. Подпись: создается и подписывается на основе заголовка и утверждений, которые закодированы в Base64.

Если вы используете веб-сайт jwt.io с токеном, указанным выше, вы можете декодировать токен и видеть его, как показано ниже:

enter image description here

Технически, JWT использует подпись, которая подписана из заголовков и требует алгоритма безопасности, указанного в заголовках (пример: HMACSHA256). Поэтому JWT необходимо передавать по HTTP, если вы храните какую-либо конфиденциальную информацию в претензиях.

Теперь, чтобы использовать аутентификацию JWT, вам не нужно промежуточное ПО OWIN, если у вас есть устаревшая система Web Api. Простая концепция заключается в том, как предоставить токен JWT и как проверить токен при поступлении запроса. Это.

Возвращаясь к демонстрации, чтобы облегчить токен JWT, я сохраняю только username и expiration time в JWT. Но таким образом, вы должны пересоздать новую локальную идентификацию (принципал), чтобы добавить больше информации, например: роли... если вы хотите выполнить авторизацию ролей. Но если вы хотите добавить больше информации в JWT, вам решать: она очень гибкая.

Вместо использования промежуточного программного обеспечения OWIN вы можете просто предоставить конечную точку токена JWT, используя действие контроллера:

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

Это наивное действие; в производстве вы должны использовать запрос POST или конечную точку базовой аутентификации для предоставления токена JWT.

Как сгенерировать токен на основе username?

Вы можете использовать пакет NuGet под названием System.IdentityModel.Tokens.Jwt от Microsoft, чтобы сгенерировать токен или даже другой пакет, если хотите. В демоверсии я использую HMACSHA256 с 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;
}

Конечная точка для предоставления токена JWT готова. Теперь, как проверить JWT, когда приходит запрос? В демоверсии я построил JwtAuthenticationAttribute который наследуется от IAuthenticationFilter (подробнее о фильтре аутентификации здесь).

С помощью этого атрибута вы можете аутентифицировать любое действие: вам просто нужно поместить этот атрибут в это действие.

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

Вы также можете использовать промежуточное программное обеспечение OWIN или DelegateHander, если вы хотите проверить все входящие запросы для вашего WebAPI (не относится к контроллеру или действию)

Ниже приведен основной метод из фильтра аутентификации:

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

Рабочий процесс заключается в использовании библиотеки JWT (пакет NuGet выше) для проверки токена JWT, а затем возврата обратно ClaimsPrincipal. Вы можете выполнить дополнительную проверку, например, проверить, существует ли пользователь в вашей системе, и добавить другие пользовательские проверки, если хотите. Код для проверки токена JWT и возврата принципала:

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

Если токен JWT проверен, а принципал возвращен, вы должны создать новый локальный идентификатор и добавить в него дополнительную информацию для проверки авторизации роли.

Не забудьте добавить config.Filters.Add(new AuthorizeAttribute()); (авторизация по умолчанию) в глобальном масштабе, чтобы предотвратить любой анонимный запрос к вашим ресурсам.

Вы можете использовать Почтальон для тестирования демо:

Запросить токен (наивно, как я уже говорил выше, только для демонстрации):

GET http://localhost:{port}/api/token?username=cuong&password=1

Поместите токен JWT в заголовок для авторизованного запроса, например:

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

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s

Демо-версия размещена здесь: https://github.com/cuongle/WebApi.Jwt.

Ответ 2

Мне удалось добиться этого с минимальными усилиями (так же просто, как с ASP.NET Core).

Для этого я использую Owin Startup.cs файл и Microsoft.Owin.Security.Jwt библиотеку.

Чтобы приложение Startup.cs нам нужно изменить Web.config:

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

Вот как должен выглядеть Startup.cs:

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

Многие из вас, ребята, используют ASP.NET Core в настоящее время, так что, как вы можете видеть, он не сильно отличается от того, что у нас там есть.

Сначала это действительно озадачило меня, я пытался внедрить пользовательских провайдеров и т.д. Но я не ожидал, что это будет так просто. OWIN просто качается!

Стоит упомянуть одну вещь - после того, как я включил OWIN Startup, библиотека NSWag перестала работать для меня (например, некоторые из вас могут захотеть автоматически генерировать HTTP-прокси для машинописного текста для приложения Angular).

Решение также было очень простым - я заменил NSWag на Swashbuckle и у меня больше не было проблем.


Хорошо, теперь ConfigHelper кодом ConfigHelper:

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

Еще один важный аспект - я отправил токен JWT через заголовок авторизации, поэтому машинописный код выглядит для меня следующим образом:

(код ниже генерируется 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')
            })
        };

См. Часть заголовков - "Authorization": "Bearer " + localStorage.getItem('token')

Ответ 3

Я думаю, вы должны использовать какой-то сторонний сервер для поддержки токена JWT, и нет никакой поддержки JWT в WEB API 2.

Однако существует проект OWIN для поддержки некоторого формата подписанного токена (не JWT). Он работает как сокращенный протокол OAuth, чтобы предоставить просто простую форму аутентификации для веб-сайта.

Вы можете узнать больше об этом, например. здесь.

Это довольно долго, но большинство деталей - это детали с контроллерами и идентификаторами ASP.NET, которые вам могут вообще не нужны. Наиболее важными являются

Шаг 9: добавьте поддержку для создания токенов маркера OAuth

Шаг 12: Тестирование Back-end API

Здесь вы можете прочитать, как настроить конечную точку (например, "/токен" ), с которой вы можете получить доступ из интерфейса (и сведения о формате запроса).

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

Ответ 4

Я также реализую API-интерфейс Jason Web Token API в своем проекте, вы можете скачать с помощью этой ссылки JWT API Token. Вы можете использовать [authorize] для проверки подлинности пользователя или нет?

Ответ 5

Есть сообщение в блоге, в котором объясняется, как использовать JWT, не используя все эти сложные вещи OAuth, и не беспокоясь о проблеме, которую OWIN имеет с обычным классом HttpContext.

Проверьте это.

Ответ 6

Здесь очень минимальная и безопасная реализация аутентификации на основе утверждений с использованием токена JWT в ASP.NET Core Web API.

Прежде всего, вам нужно предоставить конечную точку, которая возвращает токен JWT с утверждениями, назначенными пользователю:

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

Теперь вам нужно добавить Аутентификацию в ваши службы в ваших ConfigureServices внутри вашего startup.cs, чтобы добавить аутентификацию JWT в качестве службы аутентификации по умолчанию, например:

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

Теперь вы можете добавить политики к вашим сервисам авторизации, например так:

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

АЛЬТЕРНАТИВНО, Вы также можете (не обязательно) заполнять все свои заявки из вашей базы данных, поскольку они будут запускаться только один раз при запуске приложения и добавлять их в политики, подобные этой:

  services.AddAuthorization(async options =>
            {
                var ClaimList = await claimApplication.GetList(applicationClaim);
                foreach (var item in ClaimList)
                {                        
                    options.AddPolicy(item.ClaimCode, policy => policy.RequireClaim(item.ClaimCode));                       
                }
            });

Теперь вы можете установить фильтр политики для любого из методов, которые вы хотите авторизовать, например:

 [HttpPost("update")]
        [Authorize(Policy = "ACC_UP")]
        public async Task<IActionResult> Update([FromBody] UserRequestVM requestVm, CancellationToken ct)
        {
//your logic goes here
}

Надеюсь это поможет