Взгляд на middleware JWT для аутентификации в ASP.NET Core

  • Михаил
  • 12 мин. на прочтение
  • 158
  • 05 Dec 2022
  • 05 Dec 2022

Это следующая статья из серии статей об аутентификации и авторизации в ASP.NET Core. В первом посте у нас было общее введение в аутентификацию в ASP.NET Core, а затем в предыдущем посте мы более подробно рассмотрели промежуточное ПО cookie , чтобы попытаться разобраться с процессом аутентификации запроса.

В этом посте мы рассмотрим еще одно ПО промежуточного слоя, JwtBearerAuthenticationMiddleware, и снова рассмотрим, как оно реализовано в ASP.NET Core , как средство для понимания проверки подлинности в фреймворке в целом.

Что такое аутентификация носителя?

Первая концепция, которую нужно понять, — это сама аутентификация носителя, в которой используются токены носителя. Согласно спецификации токен на предъявителя это:

Маркер безопасности со свойством, согласно которому любая сторона, владеющая маркером («носитель»), может использовать маркер любым способом, которым может любая другая сторона, владеющая им. Использование токена на предъявителя не требует от предъявителя доказательства владения материалом криптографического ключа (доказательство владения).

Другими словами, предоставив действительный токен, вы будете автоматически аутентифицированы без необходимости сопоставлять или предоставлять какую-либо дополнительную подпись или данные, подтверждающие, что он был вам предоставлен. Он часто используется в структуре авторизации OAuth 2.0, например, при входе на сторонний сайт с использованием ваших учетных записей Google или Facebook.

На практике токен-носитель обычно предоставляется удаленному серверу с помощью Authorizationзаголовка HTTP:

Authorization: Bearer BEARER_TOKEN

где BEARER_TOKENнастоящий токен. Важно помнить, что токены на предъявителя дают право любому, кто ими владеет, получить доступ к ресурсу, который они защищают. Это означает, что вы должны быть уверены, что используете токены только через SSL/TLS, чтобы их нельзя было перехватить и украсть.

Что такое JWT?

Веб-токен JSON (JWT) — это веб-стандарт , определяющий метод передачи утверждений в виде объекта JSON таким образом, чтобы они могли быть криптографически подписаны или зашифрованы. Сегодня он широко используется в Интернете, в частности, во многих реализациях OAuth 2.

JWT состоят из 3 частей:

  1. Заголовок : объект JSON, который указывает тип токена ( JWT) и алгоритм, используемый для его подписи.
  2. Полезная нагрузка : объект JSON с утвержденными претензиями объекта.
  3. Подпись : строка, созданная с использованием секрета и комбинированного заголовка и полезной нагрузки. Используется для проверки того, что токен не был подделан.

Затем они кодируются base64Url и разделяются расширением .. Использование веб-токенов JSON позволяет относительно компактно отправлять утверждения и защищать их от модификации с помощью подписи. Одно из их основных преимуществ заключается в том, что они могут разрешать приложения без сохранения состояния, включая сохранение требуемых утверждений в токене, а не на стороне сервера в хранилище сеансов.

Я не буду вдаваться здесь во все подробности о токенах JWT или о структуре OAuth, так как это отдельная огромная тема. В этом посте меня больше интересует, как промежуточное ПО и обработчики взаимодействуют с инфраструктурой аутентификации ASP.NET Core. Если вы хотите узнать больше о веб-токенах JSON, я рекомендую вам посетить сайты jwt.io и auth0.com , так как на них есть полезная информация и учебные пособия.

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

{
  "alg": "HS256",
  "typ": "JWT"
}
{
  "name": "Andrew Lock"
}

может быть закодировано в следующем заголовке:

Authorisation: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQW5kcmV3IExvY2sifQ.RJJq5u9ITuNGeQmWEA4S8nnzORCpKJ2FXUthuCuCo0I

Аутентификация носителя JWT в ASP.NET Core

Вы можете добавить аутентификацию носителя JWT в свое приложение ASP.NET Core с помощью Microsoft.AspNetCore.Authentication.JwtBearerпакета. Это предоставляет промежуточное программное обеспечение, позволяющее проверять и извлекать токены носителя JWT из заголовка. В настоящее время нет встроенного механизма для создания токенов из вашего приложения, но если вам нужна эта функциональность, существует ряд возможных проектов и решений, таких как IdentityServer 4 . В качестве альтернативы вы можете создать собственное промежуточное ПО для токенов, как показано в этом посте .

После того, как вы добавили пакет в свой project.json , вам нужно добавить промежуточное ПО в свой Startupкласс. Это позволит вам проверить токен и, если он действителен, создать ClaimsPrincipleиз содержащихся в нем утверждений.

Вы можете добавить промежуточное программное обеспечение в свое приложение, используя UseJwtBearerAuthenticationметод расширения в своем Startup.Configureметоде, передав JwtBearerOptionsобъект:

app.UseJwtBearerAuthentication(new JwtBearerOptions
{
    AutomaticAuthenticate = true,
    AutomaticChallenge = true,
    TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidIssuer = "https://issuer.example.com",

        ValidateAudience = true,
        ValidAudience = "https://yourapplication.example.com",

        ValidateLifetime = true,
    }
});

На сайте доступно много вариантов JwtBearerOptions— мы рассмотрим некоторые из них более подробно позже.

Middleware JwtBearer

В предыдущем посте мы видели, что CookieAuthenticationMiddlewareнаследуется от base AuthenticationMiddleware<T>и JwtBearerMiddlewareничем не отличается. При создании ПО промежуточного слоя выполняет различные проверки предварительных условий и инициализирует некоторые значения по умолчанию. Наиболее важной проверкой является инициализация ConfigurationManager, если она еще не была установлена.

Объект ConfigurationManagerотвечает за извлечение, обновление и кэширование метаданных конфигурации, необходимых для проверки JWT, таких как эмитент и ключи подписи. Их можно либо предоставить напрямую ConfigurationManager, настроив JwtBearerOptions.Configurationсвойство, либо используя обратный канал для получения необходимых метаданных с удаленной конечной точки. Детали этой конфигурации выходят за рамки этой статьи.

Как и в промежуточном программном обеспечении для файлов cookie, промежуточное программное обеспечение реализует единственный требуемый метод из базового класса CreateHandler()и возвращает только что созданный экземпляр JwtBearerHandler.

Метод JwtBearerHandler HandleAuthenticateAsync

Опять же, как и в случае с промежуточным программным обеспечением для проверки подлинности файлов cookie, вся работа выполняется в обработчике. JwtBearerHandlerпроисходит от AuthenticationHandler<JwtBearerOptions>, переопределяя требуемый HandleAuthenticateAsync()метод.

Этот метод отвечает за десериализацию веб-токена JSON, его проверку и создание соответствующего AuthenticateResultс помощью AuthenticationTicket(если проверка прошла успешно). Мы рассмотрим большую часть этого раздела в этом разделе, но он довольно длинный, поэтому я пропущу некоторые из них!

При получении сообщения

Первый раздел HandleAuthenticateAsyncметода позволяет настроить весь метод аутентификации канала-носителя.

// Give application opportunity to find from a different location, adjust, or reject token
var messageReceivedContext = new MessageReceivedContext(Context, Options);

// event can set the token
await Options.Events.MessageReceived(messageReceivedContext);
if (messageReceivedContext.CheckEventResult(out result))
{
    return result;
}

// If application retrieved token from somewhere else, use that.
token = messageReceivedContext.Token;

Этот раздел вызывает MessageReceivedобработчик событий JwtBearerOptionsобъекта. Вам предоставляется полный HttpContext, а также сам JwtBearerOptionsобъект. Это дает вам большую гибкость в том, как ваши приложения используют токены. Вы можете проверить токен самостоятельно, используя любую другую дополнительную информацию, которая может вам потребоваться, и AuthenticateResultявно установить свойство. Если вы выберете этот подход и самостоятельно обработаете аутентификацию, метод просто вернет AuthenticateResultпосле вызова messageReceivedContext.CheckEventResult.

Кроме того, вы можете получить токен откуда-то еще, например, из другого заголовка или даже из файла cookie. В этом случае обработчик будет использовать предоставленный токен для всей дальнейшей обработки.

Читать Authorizationзаголовок

В следующем разделе, предполагая, что токен не был предоставлен messageReceivedContext, метод пытается прочитать токен из Authorizationзаголовка:

if (string.IsNullOrEmpty(token))
{
    string authorization = Request.Headers["Authorization"];

    // If no authorization header found, nothing to process further
    if (string.IsNullOrEmpty(authorization))
    {
        return AuthenticateResult.Skip();
    }

    if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
    {
        token = authorization.Substring("Bearer ".Length).Trim();
    }

    // If no token found, no further work possible
    if (string.IsNullOrEmpty(token))
    {
        return AuthenticateResult.Skip();
    }
}

Как видите, если заголовок не найден или не начинается со строки "Bearer ", то оставшаяся часть аутентификации пропускается. Аутентификация будет передаваться следующему обработчику, пока он не найдет промежуточное программное обеспечение для ее обработки.

ОбновлятьTokenValidationParameters

На данном этапе у нас есть токен, но нам еще нужно проверить и десериализовать его в файл ClaimsPrinciple. В следующем разделе HandleAuthenticationAsyncиспользуется ConfigurationManagerобъект, созданный при создании экземпляра промежуточного программного обеспечения, для обновления издателя и ключей подписи, которые будут использоваться для проверки токена:

if (_configuration == null && Options.ConfigurationManager != null)
{
    _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
}

var validationParameters = Options.TokenValidationParameters.Clone();
if (_configuration != null)
{
    if (validationParameters.ValidIssuer == null && !string.IsNullOrEmpty(_configuration.Issuer))
    {
        validationParameters.ValidIssuer = _configuration.Issuer;
    }
    else
    {
        var issuers = new[] { _configuration.Issuer };
        validationParameters.ValidIssuers = (validationParameters.ValidIssuers == null ? issuers : validationParameters.ValidIssuers.Concat(issuers));
    }

    validationParameters.IssuerSigningKeys = (validationParameters.IssuerSigningKeys == null ? _configuration.SigningKeys : validationParameters.IssuerSigningKeys.Concat(_configuration.SigningKeys));
}

Во- первых _configuration, приватное поле обновляется последними (кешированными) сведениями о конфигурации из файла ConfigurationManager. Указанное TokenValidationParametersпри настройке ПО промежуточного слоя затем клонируется для этого запроса и дополняется дополнительной конфигурацией. Любая другая проверка, указанная при добавлении промежуточного программного обеспечения, также будет проверена (например, мы включили и ValidateIssuerтребования в приведенном выше примере).ValidateAudienceValidateLifetime

Проверка токена

Теперь все настроено для проверки предоставленного токена. Объект JwtBearerOptionsсодержит список, ISecurityTokenValidatorпоэтому вы потенциально можете использовать настраиваемые валидаторы токенов, но по умолчанию используется встроенный JwtSecurityTokenHandler. Это проверит токен, подтвердит, что он соответствует всем требованиям и не был подделан, а затем вернет файл ClaimsPrinciple.

List<Exception> validationFailures = null;
SecurityToken validatedToken;
foreach (var validator in Options.SecurityTokenValidators)
{
    if (validator.CanReadToken(token))
    {
        ClaimsPrincipal principal;
        try
        {
            principal = validator.ValidateToken(token, validationParameters, out validatedToken);
        }
        catch (Exception ex)
        {
            //... Logging etc

            validationFailures = validationFailures ?? new List<Exception>(1);
            validationFailures.Add(ex);
            continue;
        }

        // See next section - returning a success result.
    }
}

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

К счастью, встроенный JwtSecurityTokenHandlerмодуль обрабатывает все сложные детали правильной реализации спецификации JWT, поэтому, если ConfigurationManagerон правильно настроен, вы сможете проверить большинство типов токенов.

Я немного упустил блок catch, но мы регистрируем ошибку, добавляем ее в validationFailuresколлекцию ошибок, потенциально обновляем конфигурацию ConfigurationManagerи пробуем следующий обработчик.

Когда проверка прошла успешно

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

Logger.TokenValidationSucceeded();

var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), Options.AuthenticationScheme);
var tokenValidatedContext = new TokenValidatedContext(Context, Options)
{
    Ticket = ticket,
    SecurityToken = validatedToken,
};

await Options.Events.TokenValidated(tokenValidatedContext);
if (tokenValidatedContext.CheckEventResult(out result))
{
    return result;
}
ticket = tokenValidatedContext.Ticket;

if (Options.SaveToken)
{
    ticket.Properties.StoreTokens(new[]
    {
        new AuthenticationToken { Name = "access_token", Value = token }
    });
}

return AuthenticateResult.Success(ticket);

Вместо того, чтобы сразу возвращать успешный результат, обработчик сначала вызывает TokenValidatedобработчик события. Это позволяет нам полностью настроить извлеченный файл ClaimsPrincipal, даже полностью заменив его или отклонив его на данном этапе, создав новый файл AuthenticateResult.

Наконец, обработчик необязательно сохраняет извлеченный токен в файле AuthenticationPropertiesдля AuthenticationTicketиспользования в другом месте фреймворка и возвращает аутентифицированный билет, используя AuthenticateResult.Success.

Когда проверка не проходит

Если токен безопасности не может быть проверен ни одним из ISecurityTokenValidators, обработчик дает еще один шанс настроить результат.

if (validationFailures != null)
{
    var authenticationFailedContext = new AuthenticationFailedContext(Context, Options)
    {
        Exception = (validationFailures.Count == 1) ? validationFailures[0] : new AggregateException(validationFailures)
    };

    await Options.Events.AuthenticationFailed(authenticationFailedContext);
    if (authenticationFailedContext.CheckEventResult(out result))
    {
        return result;
    }

    return AuthenticateResult.Fail(authenticationFailedContext.Exception);
}

return AuthenticateResult.Fail("No SecurityTokenValidator available for token: " + token ?? "[null]");

Вызывается AuthenticationFailedобработчик событий, и снова можно установить AuthenticateResultнапрямую. Если обработчик не обрабатывает событие напрямую или если не было настроенных ISecurityTokenValidators, которые могли бы обработать токен, то аутентификация не удалась.

Также стоит отметить, что любые неожиданные исключения, генерируемые обработчиками событий и т. д., приведут к аналогичному вызову до Options.Events.AuthenticationFailedтого, как исключение всплывет в стеке.

Метод JwtBearerHandler HandleUnauthorisedAsync

Другой важный метод в JwtBearerHandler, HandleUnauthorisedAsyncкоторый вызывается, когда запрос требует авторизации, но не проходит аутентификацию. В CookieAuthenticationMiddleware, этот метод перенаправляет на страницу входа, а в JwtBearerHandler, 401 будет возвращен с WWW-Authenticateзаголовком, указывающим характер ошибки, в соответствии со спецификацией .

Прежде чем вернуть ошибку 401, Options.Eventобработчик получает еще одну попытку обработать запрос с помощью вызова Options.Events.Challenge. Как и прежде, это обеспечивает отличную точку расширения, если вам это нужно, позволяя вам настраивать поведение в соответствии с вашими потребностями.

Вход и выход

Последние два метода в JwtBearerHandler, HandleSignInAsyncи HandleSignOutAsyncпросто бросают a NotSupportedExceptionпри вызове. Это имеет смысл, если учесть, что токены должны поступать из другого источника.

Чтобы эффективно «войти в систему», клиент должен запросить токен у (удаленного) эмитента и предоставить его при выполнении запросов к вашему приложению. Выход с точки зрения обработчика потребует от вас просто отказаться от токена, а не отправлять его с будущими запросами.

Резюме

В этом посте мы подробно рассмотрели JwtBearerHandlerкак средство для дальнейшего понимания того, как работает аутентификация в платформе ASP.NET Core. Редко когда вам потребуется погружаться в такие подробности при простом использовании промежуточного программного обеспечения, но, надеюсь, это поможет вам понять, что происходит под капотом, когда вы добавите его в свое приложение.