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

264

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

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

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

Амир Попович
источник

Ответы:

611

Я ответил на этот вопрос: как обезопасить веб-API ASP.NET 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 с токеном выше, вы можете декодировать токен и видеть его, как показано ниже:

введите описание изображения здесь

Технически, 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.JwtMicrosoft, чтобы сгенерировать токен, или даже другой пакет, если хотите. В демо я использую 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.

cuongle
источник
5
Хорошо объяснил @Cuong Le, но я хотел бы добавить больше: если вы используете OWIN, проверьте UseJwtBearerAuthentication, доступную в Microsoft.Owin.Security.Jwt, вы можете использовать это промежуточное ПО owin в WebAPI для автоматической проверки каждого входящего запроса. используйте класс запуска owin для регистрации промежуточного программного обеспечения
Jek
5
@AmirPopovich Вам не нужно устанавливать токен в ответе, токен нужно хранить где-то еще на стороне клиента, для Интернета вы можете поместить его в локальное хранилище, когда бы вы ни отправляли HTTP-запрос, поместите токен в заголовок.
cuongle
7
Ух ты, это самое простое объяснение, которое я видел за долгое время. +100, если бы я мог
gyozo kudor
4
@Homam: Извините за этот поздний ответ, лучший способ создать это: varhmac = new HMACSHA256();var key = Convert.ToBase64String(hmac.Key);
cuongle
4
Любой, кто использует демо-код из репозитория CuongLe, заметит, что есть ошибка, при которой запросы без заголовка авторизации не обрабатываются, что означает, что любой запрос без него может пройти (не настолько безопасная конечная точка!). От @magicleon есть запрос на удаление, чтобы решить эту проблему здесь: github.com/cuongle/WebApi.Jwt/pull/4
Chucky
11

Мне удалось добиться этого с минимальными усилиями (так же просто, как с 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кодом:

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')

Алекс Герман
источник
I replaced NSWag with Swashbuckle and didn't have any further issues.У Swashbuckle есть возможность генерировать машинописные файлы или это вы сами добавили?
раздавить
@crush swashbucle - это внутренняя библиотека, предоставляющая json, как и библиотека nuget nswag, только лучше. Для создания файла машинописного текста вы все равно должны использовать пакет nswag из npm.
Алекс Герман,
Правильно, у меня уже давно есть swashbuckle в моем проекте, похоже, вы предполагали, что он может генерировать модели TypeScript вместо nswag. Я не фанат nswag ... это тяжело. Я создал свое собственное преобразование C # -> TypeScript, которое подключено к Swashbuckle - генерирует файлы как процесс пост-сборки и публикует их в ленте npm для наших проектов. Я просто хотел убедиться, что не пропустил проект Swashbuckle, который уже делал то же самое.
раздавить
8

Вот очень минимальная и безопасная реализация аутентификации на основе утверждений с использованием токена 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
}

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

Зеешан Адиль
источник
3

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

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

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

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

Шаг 9: Добавить поддержку для генерации токенов OAuth Bearer

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

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

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

Илья Черномордик
источник
2

В моем случае JWT создается отдельным API, поэтому ASP.NET нужно только декодировать и проверить его. В отличие от принятого ответа мы используем RSA, который является несимметричным алгоритмом, поэтому SymmetricSecurityKeyупомянутый выше класс не будет работать.

Вот результат.

using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Threading;
using System.Threading.Tasks;

    public static async Task<JwtSecurityToken> VerifyAndDecodeJwt(string accessToken)
    {
        try
        {
            var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{securityApiOrigin}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
            var openIdConfig = await configurationManager.GetConfigurationAsync(CancellationToken.None);
            var validationParameters = new TokenValidationParameters()
            {
                ValidateLifetime = true,
                ValidateAudience = false,
                ValidateIssuer = false,
                RequireSignedTokens = true,
                IssuerSigningKeys = openIdConfig.SigningKeys,
            };
            new JwtSecurityTokenHandler().ValidateToken(accessToken, validationParameters, out var validToken);
            // threw on invalid, so...
            return validToken as JwtSecurityToken;
        }
        catch (Exception ex)
        {
            logger.Info(ex.Message);
            return null;
        }
    }
Рон Ньюкомб
источник