Взгляд на middleware JWT для аутентификации в ASP.NET Core
Это следующая статья из серии статей об аутентификации и авторизации в 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 частей:
- Заголовок : объект JSON, который указывает тип токена (
JWT
) и алгоритм, используемый для его подписи. - Полезная нагрузка : объект JSON с утвержденными претензиями объекта.
- Подпись : строка, созданная с использованием секрета и комбинированного заголовка и полезной нагрузки. Используется для проверки того, что токен не был подделан.
Затем они кодируются 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
.
Когда проверка не проходит
Если токен безопасности не может быть проверен ни одним из ISecurityTokenValidator
s, обработчик дает еще один шанс настроить результат.
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
напрямую. Если обработчик не обрабатывает событие напрямую или если не было настроенных ISecurityTokenValidator
s, которые могли бы обработать токен, то аутентификация не удалась.
Также стоит отметить, что любые неожиданные исключения, генерируемые обработчиками событий и т. д., приведут к аналогичному вызову до 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. Редко когда вам потребуется погружаться в такие подробности при простом использовании промежуточного программного обеспечения, но, надеюсь, это поможет вам понять, что происходит под капотом, когда вы добавите его в свое приложение.
Только полноправные пользователи могут оставлять комментарии. Аутентифицируйтесь пожалуйста, используя сервисы.