Аутентифікація JWT для веб-API ASP.NET


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.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());(авторизація за замовчуванням) у глобальному масштабі, щоб запобігти будь-якому анонімному запиту до ваших ресурсів.

Ви можете використовувати Postman для тестування демо-версії:

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

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


5
Добре пояснив @Cuong Le, але мені подобається додати більше: Якщо ви використовуєте OWIN, перевірте UseJwtBearerAuthentication, доступну в Microsoft.Owin.Security.Jwt, ви можете використовувати це програмне забезпечення з власною програмою в 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
Чакі

11

Мені вдалося досягти цього з мінімальними зусиллями (так само просто, як і з ASP.NET Core).

Для цього я використовую Startup.csфайл і Microsoft.Owin.Security.Jwtбібліотеку OWIN .

Для того, щоб програма потрапила, 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просто скелі!

Зазначимо лише одне - після того, як я ввімкнула NSWagбібліотеку запуску OWIN, перестала працювати для мене (наприклад, деякі з вас, можливо, захочуть автоматично генерувати проксі-скрипти 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 Token через заголовок авторизації , тому код шрифтового тексту шукає мене наступним чином:

(код нижче генерується 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, як і краща бібліотека nswag. Для створення файлу машинопису вам слід використовувати пакет nswag з npm.
Алекс Герман

Так, у мене в проекті вже є перев'язка, це звучало так, ніби ви припускаєте, що він може генерувати моделі TypeScript замість nswag. Я не шанувальник nswag ... це важко. Я створив власне перетворення C # -> TypeScript, яке підключено до Swashbuckle - генерує файли як процес після збирання та публікує їх у стрічку npm для наших проектів. Я просто хотів переконатися, що я не обійшов увагою проект Swashbuckle, який уже робив те саме.
розчавити

8

Ось мінімальна та безпечна реалізація аутентифікації на основі претензій за допомогою маркера JWT у веб-API Core ASP.NET Core.

Перш за все, потрібно виставити кінцеву точку, яка повертає 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

Я думаю, вам слід використовувати якийсь 3d-сервер партії для підтримки маркера JWT, і у веб-API 2 немає жодної підтримки JWT.

Однак є проект OWIN для підтримки деякого формату підписаного маркера (не JWT). Він працює як скорочений протокол OAuth для надання простої форми аутентифікації для веб-сайту.

Ви можете прочитати більше про це, наприклад, тут .

Він досить довгий, але більшість деталей - це деталі з контролерами та ідентифікацією ASP.NET, які, можливо, вам зовсім не знадобляться. Найважливіші

Крок 9: Додайте підтримку для генерації токенів OAuth Bearer

Крок 12: Тестування резервного API

Там ви можете прочитати, як налаштувати кінцеву точку (наприклад, "/ маркер"), до якої можна отримати доступ із фронтеду (та детальну інформацію про формат запиту).

Інші кроки містять детальну інформацію про те, як підключити цю кінцеву точку до бази даних тощо. Ви можете вибрати потрібні частини.


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