Авторизация в ASP.NET Core MVC
В статье описаны паттерны и приемы авторизации в ASP.NET Core MVC. Подчеркну, что рассматривается только авторизация (проверка прав пользователя) а не аутентификация, поэтому в статье не будет использования ASP.NET Identity, протоколов аутентификации и т.п. Будет много примеров серверного кода, небольшой экскурс вглубь исходников Core MVC, и тестовый проект (ссылка в конце статьи). Приглашаю интересующихся под кат.
Claims
Принципы авторизации и аутентификации в ASP.NET Core MVC не изменились по сравнению с предыдущей версией фреймворка, отличаясь лишь в деталях. Одним из относительно новых понятий является claim-based авторизация, с нее мы и начнем наше путешествие. Что же такое claim? Это пара строк «ключ-значение», в качестве ключа может выступать «FirstName», «EmailAddress» и т.п. Таким образом, claim можно трактовать как свойство пользователя, как строку с данными, или даже как некоторое утверждение вида «у пользователя есть что-то«. Знакомая многим разработчикам одномерная role-based модель органично содержится в многомерной claim-based модели: роль (утверждение вида «у пользователя есть роль X«) представляет собой один из claim и содержится в списке преопределенных System.Security.Claims.ClaimTypes. Не возбраняется создавать и свои claim.
Следующее важное понятие — identity. Это единое утверждение, содержащее набор claim. Так, identity можно трактовать как цельный документ (паспорт, водительские права и др.), в этом случае claim — строка в паспорте (дата рождения, фамилия. ). В Core MVC используется класс System.Security.Claims.ClaimsIdentity.
Еще на уровень выше находится понятие principal, обозначающее самого пользователя. Как в реальной жизни у человека может быть на руках несколько документов одновременно, так и в Core MVC — principal может содержать несколько ассоциированных с пользователем identity. Всем известное свойство HttpContext.User в Core MVC имеет тип System.Security.Claims.ClaimsPrincipal. Естественно, через principal можно получить все claim каждого identity. Набор из более чем одного identity может использоваться для разграничения доступа к различным разделам сайта/сервиса.
На диаграмме указаны лишь некоторые свойства и методы классов из пространства имен System.Security.Claims.
Зачем это все нужно? При claim-based авторизации, мы явно указываем, что пользователю необходимо иметь нужный claim (свойство пользователя) для доступа к ресурсу. В простейшем случае, проверяется сам факт наличия определенного claim, хотя возможны и куда более сложные комбинации (задаваемые при помощи policy, requirements, permissions — мы подробно рассмотрим эти понятия ниже). Пример из реальной жизни: для управления легковым авто, у человека должны быть водительские права (identity) с открытой категорией B (claim).
Подготовительные работы
Здесь и далее на протяжении статьи, мы будем настраивать доступ для различных страниц веб-сайта. Для запуска представленного кода, достаточно создать в Visual Studio 2015 новое приложение типа «ASP.NET Core Web Application», задать шаблон Web Application и тип аутентификации «No Authentication».
При использовании аутентификации «Individual User Accounts» был бы сгенерирован код для хранения и загрузки пользователей в БД посредством ASP.NET Identity, EF Core и localdb. Что является совершенно избыточным в рамках данной статьи, даже несмотря на наличие легковесного EntityFrameworkCore.InMemory решения для тестирования. Более того, нам в принципе не потребуется библиотека аутентификации ASP.NET Identity. Получение principal для авторизации можно самостоятельно эмулировать in-memory, а сериализация principal в cookie возможна стандартными средствами Core MVC. Это всё, что нужно для нашего тестирования.
Если хочется использовать ASP.NET Identity с in-memory хранилищем пользователей
Для эмуляции хранилища пользователей достаточно открыть Startup.cs и зарегистрировать сервисы-заглушки во встроенном DI-контейнере:
public void ConfigureServices(IServiceCollection services) < //включаем Identity services.AddIdentity(); //регистрируем хранилище services.AddTransient, FakeUserStore>(); services.AddTransient, FakeRoleStore>(); >
Кстати, мы всего лишь проделали ту же работу, что проделал бы вызов AddEntityFrameworkStores :
services.AddIdentity() .AddEntityFrameworkStores();
Начнем с авторизации пользователя на сайте: на GET /Home/Login нарисуем форму-заглушку, добавим кнопку для отправки пустой формы на сервер. На POST /Home/Login вручную создадим principal, identity и claim (в реальном приложении эти данные были бы получены из БД). Вызов HttpContext.Authentication.SignInAsync сериализует principal и поместит его в зашифрованный cookie, который в свою очередь будет прикреплен к ответу веб-сервера и сохранен на стороне клиента:
Создание principal-заглушки при входе пользователя на сайт
[HttpGet] [AllowAnonymous] public IActionResult Login(string returnUrl = null) < ViewData["ReturnUrl"] = returnUrl; return View(); >[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task Login(LoginViewModel vm, string returnUrl = null) < //TODO: проверка пароля, загрузка пользователя из БД, и т.д. и т.п. var claims = new List< new Claim(ClaimTypes.Name, "Fake User"), new Claim("age", "25", ClaimValueTypes.Integer) >; var identity = new ClaimsIdentity("MyCookieMiddlewareInstance"); identity.AddClaims(claims); var principal = new ClaimsPrincipal(identity); await HttpContext.Authentication.SignInAsync("MyCookieMiddlewareInstance", principal, new AuthenticationProperties < ExpiresUtc = DateTime.UtcNow.AddMinutes(20) >); _logger.LogInformation(4, "User logged in."); return RedirectToLocal(returnUrl); >
Включим cookie-аутентификацию в методе Startup.Configure(app):
app.UseCookieAuthentication(new CookieAuthenticationOptions() < AuthenticationScheme = "MyCookieMiddlewareInstance", CookieName = "MyCookieMiddlewareInstance", LoginPath = new PathString("/Home/Login/"), AccessDeniedPath = new PathString("/Home/AccessDenied/"), AutomaticAuthenticate = true, AutomaticChallenge = true >);
Этот код с небольшими модификациями будет основой для всех последующих примеров.
Атрибут Authorize и политики доступа
Атрибут [Authorize] никуда не делся из MVC. По-прежнему, при маркировке controller/action этим атрибутом — доступ внутрь получит только авторизованный пользователь. Вещи становятся интереснее, если дополнительно указать название политики (policy) — некоторого требования к claim пользователя:
[Authorize(Policy = "age-policy")] public IActionResult About()
Политики создаются в уже известном нам методе Startup.ConfigureServices :
services.AddAuthorization(options => < options.AddPolicy("age-policy", x =>< x.RequireClaim("age"); >); >);
Такая политика устанавливает, что попасть на страницу About сможет только авторизованный пользователь с claim-ом «age», при этом значение claim не учитывается. В следующем разделе, мы перейдем к примерам посложнее (наконец-то!), а сейчас разберемся, как это работает внутри?
[Authorize] — атрибут маркерный, сам по себе логики не содержащий. Нужен он лишь для того, чтобы указать MVC, к каким controller/action следует подключить AuthorizeFilter — один из встроенных фильтров Core MVC. Концепция фильтров та же, что и в предыдущих версиях фреймворка: фильтры выполняются последовательно, и позволяют выполнить код до и после обращения к controller/action. Важное отличие от middleware: фильтры имеют доступ к специфичному для MVC контексту (и выполняются, естественно, после всех middleware). Впрочем, грань между filter и middleware весьма расплывчата, так как вызов middleware возможно встроить в цепочку фильтров при помощи атрибута [MiddlewareFilter].
Вернемся к авторизации и AuthorizeFilter. Самое интересное происходит в его методе OnAuthorizationAsync:
- Из списка политик выбирается нужная на основе указанного в атрибуте [Authorize] значения (либо берется AuthorizationPolicy — политика по-умолчанию, содержащая всего одно требование с говорящим названием — DenyAnonymousAuthorizationRequirement.
- Выполняется проверка, соответствует ли набор из identity и claim-ов пользователя (например, полученных ранее из cookies запроса) требованиям политики.
Надеюсь, приведенные ссылки на исходный код дали вам представление об внутреннем устройстве фильтров в Core MVC.
Настройки политик доступа
Создание политик доступа через рассмотренный выше fluent-интерфейс не дает той гибкости, которая требуется в реальных приложениях. Конечно, можно явно указать допустимые значения claim через вызов RequireClaim(«x», params values) , можно скомбинировать через логическое И несколько условий, вызвав RequireClaim(«x»).RequireClaim(«y») . Наконец, можно навесить на controller и action разные политики, что, впрочем, приведет к той же комбинации условий через логическое И. Очевидно, что необходим более гибкий механизм создания политик, и он у нас есть: requirements и handlers.
services.AddAuthorization(options => < options.AddPolicy("age-policy", policy =>policy.Requirements.Add(new AgeRequirement(42), new FooRequirement())); >);
Requirement — не более чем DTO для передачи параметров в соответствующий handler, который в свою очередь имеет доступ к HttpContext.User и волен налагать любые проверки на principal и содержащиеся в нем identity/claim. Более того, handler может получать внешние зависимости через встроенный в Core MVC DI-контейнер:
Пример requirement и handler
public class MinAgeRequirement : IAuthorizationRequirement < public MinAgeRequirement(int age) < Age = age; >public int Age < get; private set; >> public class MinAgeHandler : AuthorizationHandler < public MinAgeHandler(IFooService fooService) < // fooService будет передан через DI >protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinAgeRequirement requirement) < bool hasClaim = context.User.HasClaim(c =>c.Type == "age"); bool hasIdentity = context.User.Identities.Any(i => i.AuthenticationType == "MultiPass"); string claimValue = context.User.FindFirst(c => c.Type == "age").Value; if (int.Parse(claimValue) >= requirement.Age) < context.Succeed(requirement); >else < context.Fail(); >return Task.CompletedTask; > >
Регистрируем сам handler в Startup.ConfigureServices(), и он готов к использованию:
services.AddSingleton();
Handler-ы возможно сочетать как через AND, так и через OR. Так, при регистрации нескольких наследников AuthorizationHandler , все они будут вызваны. При этом вызов context.Succeed() не является обязательным, а вызов context.Fail() приводит к общему отказу в авторизации вне зависимости от результата других handler. Итого, мы можем комбинировать между собой рассмотренные механизмы доступа следующим образом:
- Policy: AND
- Requirement: AND
- Handler: AND / OR.
Resource-based авторизация
Как уже говорилось ранее, policy-based авторизация выполняется Core MVC в filter pipeline, т.е. ДО вызова защищаемого action. Успех авторизации при этом зависит только от пользователя — либо он обладает нужными claim, либо нет. А что, если необходимо учесть также защищаемый ресурс и его свойства, получить какие данные из внешних источников? Пример из жизни: защищаем action вида GET /Orders/ , считывающий по id строку с заказом из БД. Пусть наличие у пользователя прав на конкретный заказ мы сможем определить только после получения этого заказа из БД. Это автоматически делает непригодными рассмотренные ранее аспектно-ориентированные сценарии на основе фильтров MVC, выполняемых перед тем, как пользовательский код получает управление. К счастью, в Core MVC есть способы провести авторизацию вручную.
Для этого, в контроллере нам потребуется реализация IAuthorizationService . Получим ее, как обычно, через внедрение зависимости в конструктор:
public class ResourceController : Controller < IAuthorizationService _authorizationService; public ResourceController(IAuthorizationService authorizationService) < _authorizationService = authorizationService; >>
Затем создадим новую политику и handler:
options.AddPolicy("resource-allow-policy", x => < x.AddRequirements(new ResourceBasedRequirement()); >); public class ResourceHandler : AuthorizationHandler < protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, ResourceBasedRequirement requirement, Order order) < // TODO: проверка, имеет ли пользователь права на действия с заказом if (true) context.Succeed(requirement); return Task.CompletedTask; >>
Наконец, проверяем пользователя + ресурс на соответствие нужной политике внутри action (заметьте, атрибут [Authorize] больше не нужен):
public async Task Allow(int id) < Order order = new Order(); //получим ресурс из БД if (await _authorizationService.AuthorizeAsync(User, order, "my-resource-policy")) < return View(); >else < //вернем 401 или 403 в зависимости от состояния пользователя return new ChallengeResult(); >>
У метода IAuthorizationService.AuthorizeAsync есть перегрузка, принимающая список из requirement — вместо названия политики:
Task AuthorizeAsync( ClaimsPrincipal user, object resource, IEnumerable requirements);
Что позволяет еще более гибко настраивать права доступа. Для демонстрации, используем преопределенный OperationAuthorizationRequirement (да, этот пример перекочевал в статью прямо с docs.microsoft.com):
public static class Operations < public static OperationAuthorizationRequirement Create = new OperationAuthorizationRequirement < Name = "Create" >; public static OperationAuthorizationRequirement Read = new OperationAuthorizationRequirement < Name = "Read" >; public static OperationAuthorizationRequirement Update = new OperationAuthorizationRequirement < Name = "Update" >; public static OperationAuthorizationRequirement Delete = new OperationAuthorizationRequirement < Name = "Delete" >; >
что позволит вытворять следующие вещи:
_authorizationService.AuthorizeAsync( User, resource, Operations.Create, Operations.Read, Operations.Update);
В методе HandleRequirementAsync(context, requirement, resource) соответствующего handler — нужно лишь проверить права соответственно операции, указанной в requirement.Name и не забыть вызвать context.Fail() если пользователь провалил авторизацию:
protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, Order order) < string operationName = requirement.Name; // Проверка, имеет ли пользователь права на действия с заказом if(true) context.Succeed(requirement); return Task.CompletedTask; >
Handler будет вызван столько раз, сколько requirement вы передали в AuthorizeAsync и проверит каждый requirement по-отдельности. Для единовременной проверки всех прав на операции за один вызов handler — передавайте список операций внутри requirement, например так:
new OperationListRequirement(new[] < Ops.Read, Ops.Update >)
На этом обзор возможностей resource-based авторизации закончен, и самое время покрыть наши handler-ы тестами:
[Test] public async Task MinAgeHandler_WhenCalledWithValidUser_Succeed() < var requirement = new MinAgeRequirement(24); var user = new ClaimsPrincipal(new ClaimsIdentity(new List< new Claim("age", "25") >)); var context = new AuthorizationHandlerContext(new [] < requirement >, user, resource: null); var handler = new MinAgeHandler(); await handler.HandleAsync(context); Assert.True(context.HasSucceeded); >
Авторизация в Razor-разметке
Выполняемая непосредственно в разметке проверка прав пользователя может быть полезна для скрытия элементов UI, к которым пользователь не должен иметь доступ. Конечно же, во view можно передать все необходимые флаги через ViewModel (при прочих равных я за этот вариант), либо обратиться напрямую к principal через HttpContext.User:
Возраст: @User.GetClaimValue("age")
Если вам интересно, то view наследуются от RazorPage класса, а прямой доступ к HttpContext из разметки возможен через свойство @Context .
С другой стороны, мы можем использовать подход из предыдущего раздела: получить реализацию IAuthorizationService через DI (да, прямо во view) и проверить пользователя на соответствие требованиям нужной политики:
@inject IAuthorizationService AuthorizationService @if (await AuthorizationService.AuthorizeAsync(User, "my-policy"))
Не пытайтесь использовать в нашем тестовом проекте вызов SignInManager.IsSignedIn(User) (используется в шаблоне веб-приложения с типом аутентификации Individual User Accounts). В первую очередь потому, что мы не используем библиотеку аутентификации Microsoft.AspNetCore.Identity , к которой этот класс принадлежит. Сам метод внутри не делает ничего, помимо проверки наличия у пользователя identity с зашитым в коде библиотеки именем.
Permission-based авторизация. Свой фильтр авторизации
Декларативное перечисление всех запрашиваемых операций (в первую очередь из числа CRUD) при авторизации пользователя, такое как:
var requirement = OperationListRequirement(new[] < Ops.FooAction, Ops.BarAction >); _authorizationService.AuthorizeAsync(User, resource, requirement);
… имеет смысл, если в вашем проекте построена система персональных разрешений (permissions): имеется некий набор из большого числа высокоуровневых операций бизнес-логики, есть пользователи (либо группы пользователей), которым были в ручном режиме выданы права на конкретные операции с конкретным ресурсом. К примеру, у Васи есть права «драить палубу», «спать в кубрике», а Петя может «крутить штурвал». Хорош или плох такой паттерн — тема для отдельной статьи (лично я от него не в восторге). Очевидная проблема данного подхода: список операций легко разрастается до нескольких сотен даже не в самой большой системе.
Ситуация упрощается, если для авторизации нет нужды учитывать конкретный экземпляр защищаемого ресурса, и наша система обладает достаточной гранулярностью, чтобы просто навесить на весь метод атрибут со списком проверяемых операций, вместо сотен вызовов AuthorizeAsync в защищаемом коде. Однако, использование авторизации на основе политик [Authorize(Policy = «foo-policy»)] приведет к комбинаторному взрыву числа политик в приложении. Почему бы не использовать старую добрую role-based авторизацию? В примере кода ниже, пользователю необходимо быть членом всех указанных ролей для получения доступа к FooController:
[Authorize(Roles = "PowerUser")] [Authorize(Roles = "ControlPanelUser")] public class FooController : Controller
Подобное решение так же может не дать достаточной детализации и гибкости для системы с большим количеством permissions и их возможных комбинаций. Дополнительные проблемы начинаются, когда нужна и role-based и permission-based авторизация. Да и семантически, роли и операции — разные вещи, хотелось бы обрабатывать их авторизацию отдельно. Решено: пишем свою версию атрибута [Authorize] ! Продемонстрирую конечный результат:
[AuthorizePermission(Permission.Foo, Permission.Bar)] public IActionResult Edit()
Начнем с создания enum для операций, requirement и handler для проверки пользователя:
Скрытый текст
public enum Permission < Foo, Bar >public class PermissionRequirement : IAuthorizationRequirement < public Permission[] Permissions < get; set; >public PermissionRequirement(Permission[] permissions) < Permissions = permissions; >> public class PermissionHandler : AuthorizationHandler < protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, PermissionRequirement requirement) < //TODO: ваш код проверки, есть ли у пользователя права на эти операции if (requirement.Permissions.Any()) < context.Succeed(requirement); >return Task.CompletedTask; > >
Ранее я рассказывал, что атрибут [Authorize] сугубо маркерный и нужен для применения AuthorizeFilter . Не будем бороться с существующей архитектурой, поэтому напишем по аналогии собственный фильтр авторизации. Поскольку список permissions у каждого action свой, то:
- Необходимо создавать экземпляр фильтра на каждый вызов;
- Невозможно напрямую создать экземпляр через встроенный DI-контейнер.
К счастью, в Core MVC эти проблемы легко разрешимы при помощи атрибута [TypeFilter]:
[TypeFilter(typeof(PermissionFilterV1), new object[] < new[] < Permission.Foo, Permission.Bar >>)] public IActionResult Index()
PermissionFilterV1
public class PermissionFilterV1 : Attribute, IAsyncAuthorizationFilter < private readonly IAuthorizationService _authService; private readonly Permission[] _permissions; public PermissionFilterV1(IAuthorizationService authService, Permission[] permissions) < _authService = authService; _permissions = permissions; >public async Task OnAuthorizationAsync(AuthorizationFilterContext context) < bool ok = await _authService.AuthorizeAsync( context.HttpContext.User, null, new PermissionRequirement(_permissions)); if (!ok) context.Result = new ChallengeResult(); >>
Мы получили полностью работающее, но безобразно выглядящее решение. Для того, чтобы скрыть детали реализации нашего фильтра от вызывающего кода, нам и пригодится атрибут [AuthorizePermission] :
public class AuthorizePermissionAttribute : TypeFilterAttribute < public AuthorizePermissionAttribute(params Permission[] permissions) : base(typeof(PermissionFilterV2)) < Arguments = new[] < new PermissionRequirement(permissions) >; Order = Int32.MaxValue; > >
[AuthorizePermission(Permission.Foo, Permission.Bar)] [Authorize(Policy = "foo-policy")] public IActionResult Index()
Обратите внимание: фильтры авторизации работают независимо, что позволяет сочетать их друг с другом. Порядок выполнения нашего фильтра в общей очереди можно скорректировать при помощи свойства AuthorizePermissionAttribute.Order .
Дополнительные материалы для чтения по теме (также приветствуются ваши ссылки для включения в список):
- Официальная документация на docs.microsoft.com.
- Цикл статей по безопасности ASP.NET Core в блоге Andrew Lock | .NET Escapades. Интересный блог в целом.
На этом обзор авторизации в ASP.NET Core MVC завершен. Большая часть материала применима и к WebAPI. Желающим воспроизвести примеры из статьи я рекомендую воспользоваться демонстрационным проектом. В следующей статье (я надеюсь) мы защитим веб-сайт и публичный API при помощи выделенного сервера аутентификации.
- asp.net core mvc
- authorization
- авторизация
Сохранение дополнительных утверждений и маркеров от внешних поставщиков в ASP.NET Core
Приложение ASP.NET Core может устанавливать дополнительные утверждения и маркеры от внешних поставщиков проверки подлинности, таких как Facebook, Google, Microsoft и Twitter. Каждый поставщик показывает различные сведения о пользователях на своей платформе, но шаблон получения и преобразования данных пользователей в дополнительные утверждения совпадает.
Необходимые компоненты
Определите, какие внешние поставщики проверки подлинности будут поддерживать в приложении. Для каждого поставщика зарегистрируйте приложение и получите идентификатор клиента и секрет клиента. Дополнительные сведения см. в статье «Проверка подлинности Facebook и Google» в ASP.NET Core. В примере приложения используется поставщик проверки подлинности Google.
Установка идентификатора клиента и секрета клиента
Поставщик проверки подлинности OAuth устанавливает отношение доверия с приложением с помощью идентификатора клиента и секрета клиента. Идентификатор клиента и значения секрета клиента создаются внешним поставщиком проверки подлинности при регистрации приложения в поставщике. Каждый внешний поставщик, который использует приложение, должен быть настроен независимо с идентификатором клиента поставщика и секретом клиента. Дополнительные сведения см. в разделах внешнего поставщика проверки подлинности, которые применяются:
- Проверка подлинности Facebook
- Проверка подлинности Google
- Проверка подлинности Майкрософт
- Проверка подлинности Twitter
- Другие поставщики проверки подлинности
- OpenId Подключение
Необязательные утверждения, отправленные в идентификатор или маркер доступа от поставщика проверки подлинности, обычно настраиваются на веб-портале поставщика. Например, Microsoft Azure Active Directory (AAD) позволяет назначать необязательные утверждения маркеру идентификатора приложения в колонке конфигурации маркера регистрации приложения. Дополнительные сведения см. в статье «Практическое руководство. Предоставление дополнительных утверждений приложению (документация Azure)». Для других поставщиков обратитесь к своим внешним наборам документации.
Пример приложения настраивает поставщика проверки подлинности Google с идентификатором клиента и секретом клиента, предоставленным Google:
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using WebGoogOauth.Data; var builder = WebApplication.CreateBuilder(args); var configuration = builder.Configuration; builder.Services.AddAuthentication().AddGoogle(googleOptions => < googleOptions.ClientId = configuration["Authentication:Google:ClientId"]; googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"]; googleOptions.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url"); googleOptions.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string"); googleOptions.SaveTokens = true; googleOptions.Events.OnCreatingTicket = ctx => < Listtokens = ctx.Properties.GetTokens().ToList(); tokens.Add(new AuthenticationToken() < Name = "TicketCreated", Value = DateTime.UtcNow.ToString() >); ctx.Properties.StoreTokens(tokens); return Task.CompletedTask; >; >); var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddDbContext(options => options.UseSqlServer(connectionString)); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true) .AddEntityFrameworkStores(); builder.Services.AddRazorPages(); var app = builder.Build(); // Remaining code removed for brevity.
Установка область проверки подлинности
Укажите список разрешений для получения от поставщика, указав параметр Scope. В следующей таблице отображаются область проверки подлинности для распространенных внешних поставщиков.
| Provider | Область |
|---|---|
| https://www.facebook.com/dialog/oauth | |
| profile , email , openid | |
| Microsoft | https://login.microsoftonline.com/common/oauth2/v2.0/authorize |
| https://api.twitter.com/oauth/authenticate |
В примере приложения Google profile email и openid область автоматически добавляются платформой при AddGoogle вызовеAuthenticationBuilder. Если приложению требуются дополнительные область, добавьте их в параметры. В следующем примере приложение Google https://www.googleapis.com/auth/user.birthday.read область добавляется для получения дня рождения пользователя:
options.Scope.Add("https://www.googleapis.com/auth/user.birthday.read");
Сопоставление ключей данных пользователя и создание утверждений
В параметрах поставщика укажите MapJsonKeyMapJsonSubKey или для каждого ключа или подраздела в пользовательских данных внешнего поставщика JSON, чтобы удостоверение приложения считывалось при входе. Дополнительные сведения о типах утверждений см. в разделе ClaimTypes.
Пример приложения создает утверждения языкового стандарта () и рисунка locale ( urn:google:locale urn:google:picture ) из данных пользователя Google и picture ключей:
builder.Services.AddAuthentication().AddGoogle(googleOptions => < googleOptions.ClientId = configuration["Authentication:Google:ClientId"]; googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"]; googleOptions.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url"); googleOptions.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string"); googleOptions.SaveTokens = true; googleOptions.Events.OnCreatingTicket = ctx => < Listtokens = ctx.Properties.GetTokens().ToList(); tokens.Add(new AuthenticationToken() < Name = "TicketCreated", Value = DateTime.UtcNow.ToString() >); ctx.Properties.StoreTokens(tokens); return Task.CompletedTask; >; >);
В Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync , а IdentityUser ( ApplicationUser ) войдет в приложение с SignInAsyncпомощью . Во время входа в систему UserManager можно хранить ApplicationUser утверждения для пользовательских данных, доступных на сайте Principal.
В примере приложения OnPostConfirmationAsync ( Account/ExternalLogin.cshtml.cs ) устанавливает утверждения языкового стандарта ( urn:google:locale ) и рисунка ( urn:google:picture ) для входа ApplicationUser , включая утверждение для GivenName:
public async Task OnPostConfirmationAsync(string returnUrl = null) < returnUrl = returnUrl ?? Url.Content("~/"); // Get the information about the user from the external login provider var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) < ErrorMessage = "Error loading external login information during confirmation."; return RedirectToPage("./Login", new < ReturnUrl = returnUrl >); > if (ModelState.IsValid) < var user = CreateUser(); await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None); await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None); var result = await _userManager.CreateAsync(user); if (result.Succeeded) < result = await _userManager.AddLoginAsync(user, info); if (result.Succeeded) < _logger.LogInformation("User created an account using provider.", info.LoginProvider); // If they exist, add claims to the user for: // Given (first) name // Locale // Picture if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName)) < await _userManager.AddClaimAsync(user, info.Principal.FindFirst(ClaimTypes.GivenName)); >if (info.Principal.HasClaim(c => c.Type == "urn:google:locale")) < await _userManager.AddClaimAsync(user, info.Principal.FindFirst("urn:google:locale")); >if (info.Principal.HasClaim(c => c.Type == "urn:google:picture")) < await _userManager.AddClaimAsync(user, info.Principal.FindFirst("urn:google:picture")); >// Include the access token in the properties // using Microsoft.AspNetCore.Authentication; var props = new AuthenticationProperties(); props.StoreTokens(info.AuthenticationTokens); props.IsPersistent = false; var userId = await _userManager.GetUserIdAsync(user); var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); var callbackUrl = Url.Page( "/Account/ConfirmEmail", pageHandler: null, values: new < area = "Identity", userId = userId, code = code >, protocol: Request.Scheme); await _emailSender.SendEmailAsync(Input.Email, "Confirm your email", $"Please confirm your account by '>clicking here."); // If account confirmation is required, we need to show the link if we don't have a real email sender if (_userManager.Options.SignIn.RequireConfirmedAccount) < return RedirectToPage("./RegisterConfirmation", new < Email = Input.Email >); > await _signInManager.SignInAsync(user, props, info.LoginProvider); return LocalRedirect(returnUrl); > > foreach (var error in result.Errors) < ModelState.AddModelError(string.Empty, error.Description); >> ProviderDisplayName = info.ProviderDisplayName; ReturnUrl = returnUrl; return Page(); >
По умолчанию утверждения пользователя хранятся в проверке подлинности cookie. Если проверка подлинности cookie слишком велика, это может привести к сбою приложения, так как:
- Браузер обнаруживает, что cookie заголовок слишком длинный.
- Общий размер запроса слишком велик.
Если для обработки запросов пользователей требуется большое количество данных пользователя:
- Ограничьте количество и размер утверждений пользователей для обработки запросов только тем, что требуется приложению.
- Используйте настраиваемый ITicketStore для по промежуточного Cookie слоя SessionStore проверки подлинности для хранения удостоверений между запросами. Сохраняйте большие объемы сведений об удостоверениях на сервере, отправляя клиенту только небольшой ключ идентификатора сеанса.
Сохранение маркера доступа
SaveTokens определяет, следует ли хранить маркеры доступа и обновления в AuthenticationProperties после успешной авторизации. SaveTokens По умолчанию устанавливается значение false , чтобы уменьшить размер окончательной проверки подлинности cookie.
Пример приложения задает значение SaveTokens true в GoogleOptions:
builder.Services.AddAuthentication().AddGoogle(googleOptions => < googleOptions.ClientId = configuration["Authentication:Google:ClientId"]; googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"]; googleOptions.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url"); googleOptions.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string"); googleOptions.SaveTokens = true; googleOptions.Events.OnCreatingTicket = ctx => < Listtokens = ctx.Properties.GetTokens().ToList(); tokens.Add(new AuthenticationToken() < Name = "TicketCreated", Value = DateTime.UtcNow.ToString() >); ctx.Properties.StoreTokens(tokens); return Task.CompletedTask; >; >);
При OnPostConfirmationAsync выполнении сохраните маркер доступа (ExternalLoginInfo.AuthenticationTokens) из внешнего поставщика в ApplicationUser ‘s AuthenticationProperties .
Пример приложения сохраняет маркер доступа в OnPostConfirmationAsync (новая регистрация пользователя) и OnGetCallbackAsync (ранее зарегистрированный пользователь) в Account/ExternalLogin.cshtml.cs :
public async Task OnPostConfirmationAsync(string returnUrl = null) < returnUrl = returnUrl ?? Url.Content("~/"); // Get the information about the user from the external login provider var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) < ErrorMessage = "Error loading external login information during confirmation."; return RedirectToPage("./Login", new < ReturnUrl = returnUrl >); > if (ModelState.IsValid) < var user = CreateUser(); await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None); await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None); var result = await _userManager.CreateAsync(user); if (result.Succeeded) < result = await _userManager.AddLoginAsync(user, info); if (result.Succeeded) < _logger.LogInformation("User created an account using provider.", info.LoginProvider); // If they exist, add claims to the user for: // Given (first) name // Locale // Picture if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName)) < await _userManager.AddClaimAsync(user, info.Principal.FindFirst(ClaimTypes.GivenName)); >if (info.Principal.HasClaim(c => c.Type == "urn:google:locale")) < await _userManager.AddClaimAsync(user, info.Principal.FindFirst("urn:google:locale")); >if (info.Principal.HasClaim(c => c.Type == "urn:google:picture")) < await _userManager.AddClaimAsync(user, info.Principal.FindFirst("urn:google:picture")); >// Include the access token in the properties // using Microsoft.AspNetCore.Authentication; var props = new AuthenticationProperties(); props.StoreTokens(info.AuthenticationTokens); props.IsPersistent = false; var userId = await _userManager.GetUserIdAsync(user); var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); var callbackUrl = Url.Page( "/Account/ConfirmEmail", pageHandler: null, values: new < area = "Identity", userId = userId, code = code >, protocol: Request.Scheme); await _emailSender.SendEmailAsync(Input.Email, "Confirm your email", $"Please confirm your account by '>clicking here."); // If account confirmation is required, we need to show the link if we don't have a real email sender if (_userManager.Options.SignIn.RequireConfirmedAccount) < return RedirectToPage("./RegisterConfirmation", new < Email = Input.Email >); > await _signInManager.SignInAsync(user, props, info.LoginProvider); return LocalRedirect(returnUrl); > > foreach (var error in result.Errors) < ModelState.AddModelError(string.Empty, error.Description); >> ProviderDisplayName = info.ProviderDisplayName; ReturnUrl = returnUrl; return Page(); >
Сведения о передаче маркеров Razor компонентам серверного приложения см. в дополнительных сценариях безопасности на стороне Blazor сервера ASP.NET CoreBlazor.
Добавление дополнительных пользовательских маркеров
Чтобы продемонстрировать, как добавить пользовательский маркер, который хранится в составе SaveTokens , пример приложения добавляет текущее DateTimeAuthenticationToken значение для AuthenticationToken.Name TicketCreated :
builder.Services.AddAuthentication().AddGoogle(googleOptions => < googleOptions.ClientId = configuration["Authentication:Google:ClientId"]; googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"]; googleOptions.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url"); googleOptions.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string"); googleOptions.SaveTokens = true; googleOptions.Events.OnCreatingTicket = ctx => < Listtokens = ctx.Properties.GetTokens().ToList(); tokens.Add(new AuthenticationToken() < Name = "TicketCreated", Value = DateTime.UtcNow.ToString() >); ctx.Properties.StoreTokens(tokens); return Task.CompletedTask; >; >);
Создание и добавление утверждений
Платформа предоставляет общие действия и методы расширения для создания и добавления утверждений в коллекцию. Дополнительные сведения см. в разделах ClaimActionCollectionMapExtensions и ClaimActionCollectionUniqueExtensions.
Пользователи могут определять пользовательские действия, исходя из ClaimAction абстрактного Run метода и реализуя его.
Добавление и обновление утверждений пользователей
Утверждения копируются из внешних поставщиков в пользовательную базу данных при первой регистрации, а не при входе. Если в приложении включены дополнительные утверждения после регистрации пользователя для использования приложения, вызовите SignInManager.RefreshSignInAsync для пользователя принудительное создание новой проверки подлинности cookie.
В среде разработки, работающей с тестовыми учетными записями пользователей, удалите и повторно создайте учетную запись пользователя. Для рабочих систем новые утверждения, добавленные в приложение, можно заполнить в учетные записи пользователей. После создания шаблонов страницы ExternalLogin в приложение Areas/Pages/Identity/Account/Manage добавьте следующий код ExternalLoginModel в ExternalLogin.cshtml.cs файл.
Добавьте словарь добавленных утверждений. Используйте ключи словаря для хранения типов утверждений и использования значений для хранения значения по умолчанию. Добавьте следующую строку в начало класса. В следующем примере предполагается, что одно утверждение добавляется для изображения Google пользователя с универсальным изображением headshot в качестве значения по умолчанию:
private readonly IReadOnlyDictionary _claimsToSync = new Dictionary() < < "urn:google:picture", "https://localhost:5001/headshot.png" >, >;
Замените код OnGetCallbackAsync метода по умолчанию следующим кодом. Код циклит по словарю утверждений. Утверждения добавляются (резервная копия) или обновляются для каждого пользователя. При добавлении или обновлении утверждений вход пользователя обновляется с помощью SignInManager существующего свойства проверки подлинности ( AuthenticationProperties ).
private readonly IReadOnlyDictionary _claimsToSync = new Dictionary() < < "urn:google:picture", "https://localhost:5001/headshot.png" >, >; public async Task OnGetCallbackAsync(string returnUrl = null, string remoteError = null) < returnUrl = returnUrl ?? Url.Content("~/"); if (remoteError != null) < ErrorMessage = $"Error from external provider: "; return RedirectToPage("./Login", new < ReturnUrl = returnUrl >); > var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) < ErrorMessage = "Error loading external login information."; return RedirectToPage("./Login", new < ReturnUrl = returnUrl >); > // Sign in the user with this external login provider if the user already has a login. var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true); if (result.Succeeded) < _logger.LogInformation("logged in with provider.", info.Principal.Identity.Name, info.LoginProvider); if (_claimsToSync.Count > 0) < var user = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey); var userClaims = await _userManager.GetClaimsAsync(user); bool refreshSignIn = false; foreach (var addedClaim in _claimsToSync) < var userClaim = userClaims .FirstOrDefault(c =>c.Type == addedClaim.Key); if (info.Principal.HasClaim(c => c.Type == addedClaim.Key)) < var externalClaim = info.Principal.FindFirst(addedClaim.Key); if (userClaim == null) < await _userManager.AddClaimAsync(user, new Claim(addedClaim.Key, externalClaim.Value)); refreshSignIn = true; >else if (userClaim.Value != externalClaim.Value) < await _userManager .ReplaceClaimAsync(user, userClaim, externalClaim); refreshSignIn = true; >> else if (userClaim == null) < // Fill with a default value await _userManager.AddClaimAsync(user, new Claim(addedClaim.Key, addedClaim.Value)); refreshSignIn = true; >> if (refreshSignIn) < await _signInManager.RefreshSignInAsync(user); >> return LocalRedirect(returnUrl); > if (result.IsLockedOut) < return RedirectToPage("./Lockout"); >else < // If the user does not have an account, then ask the user to create an account. ReturnUrl = returnUrl; ProviderDisplayName = info.ProviderDisplayName; if (info.Principal.HasClaim(c =>c.Type == ClaimTypes.Email)) < Input = new InputModel < Email = info.Principal.FindFirstValue(ClaimTypes.Email) >; > return Page(); > >
Аналогичный подход применяется при изменении утверждений во время входа пользователя, но шаг обратной заполнения не требуется. Чтобы обновить утверждения пользователя, вызовите следующее для пользователя:
- UserManager.ReplaceClaimAsync для утверждений, хранящихся в базе данных удостоверений.
- SignInManager.RefreshSignInAsync для пользователя, чтобы принудительно создать новую проверку подлинности cookie.
Удаление действий и утверждений утверждений
ClaimActionCollection.Remove(String) удаляет все действия утверждения для заданной ClaimType коллекции. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) удаляет утверждение заданного ClaimType удостоверения. DeleteClaimв основном используется с openID Подключение (OIDC) для удаления утверждений, созданных протоколом.
Пример выходных данных приложения
Запустите пример приложения и выберите ссылку MyClaims:
User Claims http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier 9b342344f-7aab-43c2-1ac1-ba75912ca999 http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name someone@gmail.com AspNet.Identity.SecurityStamp 7D4312MOWRYYBFI1KXRPHGOSTBVWSFDE http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname Judy urn:google:locale en urn:google:picture https://lh4.googleusercontent.com/-XXXXXX/XXXXXX/XXXXXX/XXXXXX/photo.jpg Authentication Properties .Token.access_token yc23.AlvoZqz56. 1lxltXV7D-ZWP9 .Token.token_type Bearer .Token.expires_at 2019-04-11T22:14:51.0000000+00:00 .Token.TicketCreated 4/11/2019 9:14:52 PM .TokenNames access_token;token_type;expires_at;TicketCreated .persistent .issued Thu, 11 Apr 2019 20:51:06 GMT .expires Thu, 25 Apr 2019 20:51:06 GMT
Переадресация сведений запроса с помощью прокси-сервера или подсистемы балансировки нагрузки
Если приложение развертывается с прокси-сервером или подсистемой балансировки нагрузки, некоторые сведения из исходного запроса можно перенаправить в приложение в заголовках запроса. Эти сведения обычно включают безопасную схему запроса ( https ), узел и IP-адрес клиента. Приложения не считывают автоматически и не используют эти заголовки запроса.
Схема используется для создания ссылок, определяющих процесс проверки подлинности с помощью внешних поставщиков. Потеря безопасной схемы ( https ) приводит к тому, что приложение неправильно выполняет перенаправление на небезопасные URL-адреса.
Используйте ПО промежуточного слоя для перенаправленных заголовков, чтобы предоставить приложению сведения из исходных запросов для обработки запросов.
Приложение ASP.NET Core может устанавливать дополнительные утверждения и маркеры от внешних поставщиков проверки подлинности, таких как Facebook, Google, Microsoft и Twitter. Каждый поставщик показывает различные сведения о пользователях на своей платформе, но шаблон получения и преобразования данных пользователей в дополнительные утверждения совпадает.
Необходимые компоненты
Определите, какие внешние поставщики проверки подлинности будут поддерживать в приложении. Для каждого поставщика зарегистрируйте приложение и получите идентификатор клиента и секрет клиента. Дополнительные сведения см. в статье «Проверка подлинности Facebook и Google» в ASP.NET Core. В примере приложения используется поставщик проверки подлинности Google.
Установка идентификатора клиента и секрета клиента
Поставщик проверки подлинности OAuth устанавливает отношение доверия с приложением с помощью идентификатора клиента и секрета клиента. Идентификатор клиента и значения секрета клиента создаются внешним поставщиком проверки подлинности при регистрации приложения в поставщике. Каждый внешний поставщик, который использует приложение, должен быть настроен независимо с идентификатором клиента поставщика и секретом клиента. Дополнительные сведения см. в разделах внешнего поставщика проверки подлинности, которые применяются к вашему сценарию:
- Проверка подлинности Facebook
- Проверка подлинности Google
- Проверка подлинности Майкрософт
- Проверка подлинности Twitter
- Другие поставщики проверки подлинности
- OpenId Подключение
Необязательные утверждения, отправленные в идентификатор или маркер доступа от поставщика проверки подлинности, обычно настраиваются на веб-портале поставщика. Например, Microsoft Azure Active Directory (AAD) позволяет назначать необязательные утверждения маркеру идентификатора приложения в колонке конфигурации маркера регистрации приложения. Дополнительные сведения см. в статье «Практическое руководство. Предоставление дополнительных утверждений приложению (документация Azure)». Для других поставщиков обратитесь к своим внешним наборам документации.
Пример приложения настраивает поставщика проверки подлинности Google с идентификатором клиента и секретом клиента, предоставленным Google:
services.AddAuthentication().AddGoogle(options => < // Provide the Google Client ID options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com"; // Register with User Secrets using: // dotnet user-secrets set "Authentication:Google:ClientId" "" // Provide the Google Client Secret options.ClientSecret = ""; // Register with User Secrets using: // dotnet user-secrets set "Authentication:Google:ClientSecret" "" options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url"); options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string"); options.SaveTokens = true; options.Events.OnCreatingTicket = ctx => < Listtokens = ctx.Properties.GetTokens().ToList(); tokens.Add(new AuthenticationToken() < Name = "TicketCreated", Value = DateTime.UtcNow.ToString() >); ctx.Properties.StoreTokens(tokens); return Task.CompletedTask; >; >);
Установка область проверки подлинности
Укажите список разрешений для получения от поставщика, указав параметр Scope. В следующей таблице отображаются область проверки подлинности для распространенных внешних поставщиков.
| Provider | Область |
|---|---|
| https://www.facebook.com/dialog/oauth | |
| profile , email , openid | |
| Microsoft | https://login.microsoftonline.com/common/oauth2/v2.0/authorize |
| https://api.twitter.com/oauth/authenticate |
В примере приложения Google profile email и openid область автоматически добавляются платформой при AddGoogle вызовеAuthenticationBuilder. Если приложению требуются дополнительные область, добавьте их в параметры. В следующем примере приложение Google https://www.googleapis.com/auth/user.birthday.read область добавляется для получения дня рождения пользователя:
options.Scope.Add("https://www.googleapis.com/auth/user.birthday.read");
Сопоставление ключей данных пользователя и создание утверждений
В параметрах поставщика укажите MapJsonKeyMapJsonSubKey или для каждого ключа или подраздела в пользовательских данных внешнего поставщика JSON, чтобы удостоверение приложения считывалось при входе. Дополнительные сведения о типах утверждений см. в разделе ClaimTypes.
Пример приложения создает утверждения языкового стандарта () и рисунка locale ( urn:google:locale urn:google:picture ) из данных пользователя Google и picture ключей:
services.AddAuthentication().AddGoogle(options => < // Provide the Google Client ID options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com"; // Register with User Secrets using: // dotnet user-secrets set "Authentication:Google:ClientId" "" // Provide the Google Client Secret options.ClientSecret = ""; // Register with User Secrets using: // dotnet user-secrets set "Authentication:Google:ClientSecret" "" options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url"); options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string"); options.SaveTokens = true; options.Events.OnCreatingTicket = ctx => < Listtokens = ctx.Properties.GetTokens().ToList(); tokens.Add(new AuthenticationToken() < Name = "TicketCreated", Value = DateTime.UtcNow.ToString() >); ctx.Properties.StoreTokens(tokens); return Task.CompletedTask; >; >);
В Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync , а IdentityUser ( ApplicationUser ) войдет в приложение с SignInAsyncпомощью . Во время входа в систему UserManager можно хранить ApplicationUser утверждения для пользовательских данных, доступных на сайте Principal.
В примере приложения OnPostConfirmationAsync ( Account/ExternalLogin.cshtml.cs ) устанавливает утверждения языкового стандарта ( urn:google:locale ) и рисунка ( urn:google:picture ) для входа ApplicationUser , включая утверждение для GivenName:
public async Task OnPostConfirmationAsync(string returnUrl = null) < returnUrl = returnUrl ?? Url.Content("~/"); // Get the information about the user from the external login provider var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) < ErrorMessage = "Error loading external login information during confirmation."; return RedirectToPage("./Login", new < ReturnUrl = returnUrl >); > if (ModelState.IsValid) < var user = new IdentityUser < UserName = Input.Email, Email = Input.Email >; var result = await _userManager.CreateAsync(user); if (result.Succeeded) < result = await _userManager.AddLoginAsync(user, info); if (result.Succeeded) < // If they exist, add claims to the user for: // Given (first) name // Locale // Picture if (info.Principal.HasClaim(c =>c.Type == ClaimTypes.GivenName)) < await _userManager.AddClaimAsync(user, info.Principal.FindFirst(ClaimTypes.GivenName)); >if (info.Principal.HasClaim(c => c.Type == "urn:google:locale")) < await _userManager.AddClaimAsync(user, info.Principal.FindFirst("urn:google:locale")); >if (info.Principal.HasClaim(c => c.Type == "urn:google:picture")) < await _userManager.AddClaimAsync(user, info.Principal.FindFirst("urn:google:picture")); >// Include the access token in the properties var props = new AuthenticationProperties(); props.StoreTokens(info.AuthenticationTokens); props.IsPersistent = true; await _signInManager.SignInAsync(user, props); _logger.LogInformation( "User created an account using provider.", info.LoginProvider); return LocalRedirect(returnUrl); > > foreach (var error in result.Errors) < ModelState.AddModelError(string.Empty, error.Description); >> LoginProvider = info.LoginProvider; ReturnUrl = returnUrl; return Page(); >
По умолчанию утверждения пользователя хранятся в проверке подлинности cookie. Если проверка подлинности cookie слишком велика, это может привести к сбою приложения, так как:
- Браузер обнаруживает, что cookie заголовок слишком длинный.
- Общий размер запроса слишком велик.
Если для обработки запросов пользователей требуется большое количество данных пользователя:
- Ограничьте количество и размер утверждений пользователей для обработки запросов только тем, что требуется приложению.
- Используйте настраиваемый ITicketStore для по промежуточного Cookie слоя SessionStore проверки подлинности для хранения удостоверений между запросами. Сохраняйте большие объемы сведений об удостоверениях на сервере, отправляя клиенту только небольшой ключ идентификатора сеанса.
Сохранение маркера доступа
SaveTokens определяет, следует ли хранить маркеры доступа и обновления в AuthenticationProperties после успешной авторизации. SaveTokens По умолчанию устанавливается значение false , чтобы уменьшить размер окончательной проверки подлинности cookie.
Пример приложения задает значение SaveTokens true в GoogleOptions:
services.AddAuthentication().AddGoogle(options => < // Provide the Google Client ID options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com"; // Register with User Secrets using: // dotnet user-secrets set "Authentication:Google:ClientId" "" // Provide the Google Client Secret options.ClientSecret = ""; // Register with User Secrets using: // dotnet user-secrets set "Authentication:Google:ClientSecret" "" options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url"); options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string"); options.SaveTokens = true; options.Events.OnCreatingTicket = ctx => < Listtokens = ctx.Properties.GetTokens().ToList(); tokens.Add(new AuthenticationToken() < Name = "TicketCreated", Value = DateTime.UtcNow.ToString() >); ctx.Properties.StoreTokens(tokens); return Task.CompletedTask; >; >);
При OnPostConfirmationAsync выполнении сохраните маркер доступа (ExternalLoginInfo.AuthenticationTokens) из внешнего поставщика в ApplicationUser ‘s AuthenticationProperties .
Пример приложения сохраняет маркер доступа в OnPostConfirmationAsync (новая регистрация пользователя) и OnGetCallbackAsync (ранее зарегистрированный пользователь) в Account/ExternalLogin.cshtml.cs :
public async Task OnPostConfirmationAsync(string returnUrl = null) < returnUrl = returnUrl ?? Url.Content("~/"); // Get the information about the user from the external login provider var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) < ErrorMessage = "Error loading external login information during confirmation."; return RedirectToPage("./Login", new < ReturnUrl = returnUrl >); > if (ModelState.IsValid) < var user = new IdentityUser < UserName = Input.Email, Email = Input.Email >; var result = await _userManager.CreateAsync(user); if (result.Succeeded) < result = await _userManager.AddLoginAsync(user, info); if (result.Succeeded) < // If they exist, add claims to the user for: // Given (first) name // Locale // Picture if (info.Principal.HasClaim(c =>c.Type == ClaimTypes.GivenName)) < await _userManager.AddClaimAsync(user, info.Principal.FindFirst(ClaimTypes.GivenName)); >if (info.Principal.HasClaim(c => c.Type == "urn:google:locale")) < await _userManager.AddClaimAsync(user, info.Principal.FindFirst("urn:google:locale")); >if (info.Principal.HasClaim(c => c.Type == "urn:google:picture")) < await _userManager.AddClaimAsync(user, info.Principal.FindFirst("urn:google:picture")); >// Include the access token in the properties var props = new AuthenticationProperties(); props.StoreTokens(info.AuthenticationTokens); props.IsPersistent = true; await _signInManager.SignInAsync(user, props); _logger.LogInformation( "User created an account using provider.", info.LoginProvider); return LocalRedirect(returnUrl); > > foreach (var error in result.Errors) < ModelState.AddModelError(string.Empty, error.Description); >> LoginProvider = info.LoginProvider; ReturnUrl = returnUrl; return Page(); >
Сведения о передаче маркеров Razor компонентам серверного приложения см. в дополнительных сценариях безопасности на стороне Blazor сервера ASP.NET CoreBlazor.
Добавление дополнительных пользовательских маркеров
Чтобы продемонстрировать, как добавить пользовательский маркер, который хранится в составе SaveTokens , пример приложения добавляет текущее DateTimeAuthenticationToken значение для AuthenticationToken.Name TicketCreated :
services.AddAuthentication().AddGoogle(options => < // Provide the Google Client ID options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com"; // Register with User Secrets using: // dotnet user-secrets set "Authentication:Google:ClientId" "" // Provide the Google Client Secret options.ClientSecret = ""; // Register with User Secrets using: // dotnet user-secrets set "Authentication:Google:ClientSecret" "" options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url"); options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string"); options.SaveTokens = true; options.Events.OnCreatingTicket = ctx => < Listtokens = ctx.Properties.GetTokens().ToList(); tokens.Add(new AuthenticationToken() < Name = "TicketCreated", Value = DateTime.UtcNow.ToString() >); ctx.Properties.StoreTokens(tokens); return Task.CompletedTask; >; >);
Создание и добавление утверждений
Платформа предоставляет общие действия и методы расширения для создания и добавления утверждений в коллекцию. Дополнительные сведения см. в разделах ClaimActionCollectionMapExtensions и ClaimActionCollectionUniqueExtensions.
Пользователи могут определять пользовательские действия, исходя из ClaimAction абстрактного Run метода и реализуя его.
Добавление и обновление утверждений пользователей
Утверждения копируются из внешних поставщиков в пользовательную базу данных при первой регистрации, а не при входе. Если в приложении включены дополнительные утверждения после регистрации пользователя для использования приложения, вызовите SignInManager.RefreshSignInAsync для пользователя принудительное создание новой проверки подлинности cookie.
В среде разработки, работающей с тестовых учетных записей пользователей, можно просто удалить и повторно создать учетную запись пользователя. Для рабочих систем новые утверждения, добавленные в приложение, можно заполнить в учетные записи пользователей. После создания шаблонов страницы ExternalLogin в приложение Areas/Pages/Identity/Account/Manage добавьте следующий код ExternalLoginModel в ExternalLogin.cshtml.cs файл.
Добавьте словарь добавленных утверждений. Используйте ключи словаря для хранения типов утверждений и использования значений для хранения значения по умолчанию. Добавьте следующую строку в начало класса. В следующем примере предполагается, что одно утверждение добавляется для изображения Google пользователя с универсальным изображением headshot в качестве значения по умолчанию:
private readonly IReadOnlyDictionary _claimsToSync = new Dictionary() < < "urn:google:picture", "https://localhost:5001/headshot.png" >, >;
Замените код OnGetCallbackAsync метода по умолчанию следующим кодом. Код циклит по словарю утверждений. Утверждения добавляются (резервная копия) или обновляются для каждого пользователя. При добавлении или обновлении утверждений вход пользователя обновляется с помощью SignInManager существующего свойства проверки подлинности ( AuthenticationProperties ).
public async Task OnGetCallbackAsync( string returnUrl = null, string remoteError = null) < returnUrl = returnUrl ?? Url.Content("~/"); if (remoteError != null) < ErrorMessage = $"Error from external provider: "; return RedirectToPage("./Login", new ); > var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) < ErrorMessage = "Error loading external login information."; return RedirectToPage("./Login", new < ReturnUrl = returnUrl >); > // Sign in the user with this external login provider if the user already has a // login. var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor : true); if (result.Succeeded) < _logger.LogInformation("logged in with provider.", info.Principal.Identity.Name, info.LoginProvider); if (_claimsToSync.Count > 0) < var user = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey); var userClaims = await _userManager.GetClaimsAsync(user); bool refreshSignIn = false; foreach (var addedClaim in _claimsToSync) < var userClaim = userClaims .FirstOrDefault(c =>c.Type == addedClaim.Key); if (info.Principal.HasClaim(c => c.Type == addedClaim.Key)) < var externalClaim = info.Principal.FindFirst(addedClaim.Key); if (userClaim == null) < await _userManager.AddClaimAsync(user, new Claim(addedClaim.Key, externalClaim.Value)); refreshSignIn = true; >else if (userClaim.Value != externalClaim.Value) < await _userManager .ReplaceClaimAsync(user, userClaim, externalClaim); refreshSignIn = true; >> else if (userClaim == null) < // Fill with a default value await _userManager.AddClaimAsync(user, new Claim(addedClaim.Key, addedClaim.Value)); refreshSignIn = true; >> if (refreshSignIn) < await _signInManager.RefreshSignInAsync(user); >> return LocalRedirect(returnUrl); > if (result.IsLockedOut) < return RedirectToPage("./Lockout"); >else < // If the user does not have an account, then ask the user to create an // account. ReturnUrl = returnUrl; ProviderDisplayName = info.ProviderDisplayName; if (info.Principal.HasClaim(c =>c.Type == ClaimTypes.Email)) < Input = new InputModel < Email = info.Principal.FindFirstValue(ClaimTypes.Email) >; > return Page(); > >
Аналогичный подход применяется при изменении утверждений во время входа пользователя, но шаг обратной заполнения не требуется. Чтобы обновить утверждения пользователя, вызовите следующее для пользователя:
- UserManager.ReplaceClaimAsync для утверждений, хранящихся в базе данных удостоверений.
- SignInManager.RefreshSignInAsync для пользователя, чтобы принудительно создать новую проверку подлинности cookie.
Удаление действий и утверждений утверждений
ClaimActionCollection.Remove(String) удаляет все действия утверждения для заданной ClaimType коллекции. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) удаляет утверждение заданного ClaimType удостоверения. DeleteClaimв основном используется с openID Подключение (OIDC) для удаления утверждений, созданных протоколом.
Пример выходных данных приложения
User Claims http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier 9b342344f-7aab-43c2-1ac1-ba75912ca999 http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name someone@gmail.com AspNet.Identity.SecurityStamp 7D4312MOWRYYBFI1KXRPHGOSTBVWSFDE http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname Judy urn:google:locale en urn:google:picture https://lh4.googleusercontent.com/-XXXXXX/XXXXXX/XXXXXX/XXXXXX/photo.jpg Authentication Properties .Token.access_token yc23.AlvoZqz56. 1lxltXV7D-ZWP9 .Token.token_type Bearer .Token.expires_at 2019-04-11T22:14:51.0000000+00:00 .Token.TicketCreated 4/11/2019 9:14:52 PM .TokenNames access_token;token_type;expires_at;TicketCreated .persistent .issued Thu, 11 Apr 2019 20:51:06 GMT .expires Thu, 25 Apr 2019 20:51:06 GMT
Переадресация сведений запроса с помощью прокси-сервера или подсистемы балансировки нагрузки
Если приложение развертывается с прокси-сервером или подсистемой балансировки нагрузки, некоторые сведения из исходного запроса можно перенаправить в приложение в заголовках запроса. Эти сведения обычно включают безопасную схему запроса ( https ), узел и IP-адрес клиента. Приложения не считывают автоматически и не используют эти заголовки запроса.
Схема используется для создания ссылок, определяющих процесс проверки подлинности с помощью внешних поставщиков. Потеря безопасной схемы ( https ) приводит к тому, что приложение неправильно выполняет перенаправление на небезопасные URL-адреса.
Используйте ПО промежуточного слоя для перенаправленных заголовков, чтобы предоставить приложению сведения из исходных запросов для обработки запросов.
Приложение ASP.NET Core может устанавливать дополнительные утверждения и маркеры от внешних поставщиков проверки подлинности, таких как Facebook, Google, Microsoft и Twitter. Каждый поставщик показывает различные сведения о пользователях на своей платформе, но шаблон получения и преобразования данных пользователей в дополнительные утверждения совпадает.
Необходимые компоненты
Определите, какие внешние поставщики проверки подлинности будут поддерживать в приложении. Для каждого поставщика зарегистрируйте приложение и получите идентификатор клиента и секрет клиента. Дополнительные сведения см. в статье «Проверка подлинности Facebook и Google» в ASP.NET Core. В примере приложения используется поставщик проверки подлинности Google.
Установка идентификатора клиента и секрета клиента
Поставщик проверки подлинности OAuth устанавливает отношение доверия с приложением с помощью идентификатора клиента и секрета клиента. Идентификатор клиента и значения секрета клиента создаются внешним поставщиком проверки подлинности при регистрации приложения в поставщике. Каждый внешний поставщик, который использует приложение, должен быть настроен независимо с идентификатором клиента поставщика и секретом клиента. Дополнительные сведения см. в разделах внешнего поставщика проверки подлинности, которые применяются к вашему сценарию:
- Проверка подлинности Facebook
- Проверка подлинности Google
- Проверка подлинности Майкрософт
- Проверка подлинности Twitter
- Другие поставщики проверки подлинности
- OpenId Подключение
Пример приложения настраивает поставщика проверки подлинности Google с идентификатором клиента и секретом клиента, предоставленным Google:
services.AddAuthentication().AddGoogle(options => < // Provide the Google Client ID options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com"; // Register with User Secrets using: // dotnet user-secrets set "Authentication:Google:ClientId" "" // Provide the Google Client Secret options.ClientSecret = ""; // Register with User Secrets using: // dotnet user-secrets set "Authentication:Google:ClientSecret" "" options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url"); options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string"); options.SaveTokens = true; options.Events.OnCreatingTicket = ctx => < Listtokens = ctx.Properties.GetTokens().ToList(); tokens.Add(new AuthenticationToken() < Name = "TicketCreated", Value = DateTime.UtcNow.ToString() >); ctx.Properties.StoreTokens(tokens); return Task.CompletedTask; >; >);
Установка область проверки подлинности
Укажите список разрешений для получения от поставщика, указав параметр Scope. В следующей таблице отображаются область проверки подлинности для распространенных внешних поставщиков.
| Provider | Область |
|---|---|
| https://www.facebook.com/dialog/oauth | |
| https://www.googleapis.com/auth/userinfo.profile | |
| Microsoft | https://login.microsoftonline.com/common/oauth2/v2.0/authorize |
| https://api.twitter.com/oauth/authenticate |
В примере приложения область Google userinfo.profile автоматически добавляется платформой при AddGoogle вызовеAuthenticationBuilder. Если приложению требуются дополнительные область, добавьте их в параметры. В следующем примере область Google https://www.googleapis.com/auth/user.birthday.read добавляется для получения дня рождения пользователя:
options.Scope.Add("https://www.googleapis.com/auth/user.birthday.read");
Сопоставление ключей данных пользователя и создание утверждений
В параметрах поставщика укажите MapJsonKeyMapJsonSubKey или для каждого ключа или подраздела в пользовательских данных внешнего поставщика JSON, чтобы удостоверение приложения считывалось при входе. Дополнительные сведения о типах утверждений см. в разделе ClaimTypes.
Пример приложения создает утверждения языкового стандарта () и рисунка locale ( urn:google:locale urn:google:picture ) из данных пользователя Google и picture ключей:
services.AddAuthentication().AddGoogle(options => < // Provide the Google Client ID options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com"; // Register with User Secrets using: // dotnet user-secrets set "Authentication:Google:ClientId" "" // Provide the Google Client Secret options.ClientSecret = ""; // Register with User Secrets using: // dotnet user-secrets set "Authentication:Google:ClientSecret" "" options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url"); options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string"); options.SaveTokens = true; options.Events.OnCreatingTicket = ctx => < Listtokens = ctx.Properties.GetTokens().ToList(); tokens.Add(new AuthenticationToken() < Name = "TicketCreated", Value = DateTime.UtcNow.ToString() >); ctx.Properties.StoreTokens(tokens); return Task.CompletedTask; >; >);
В Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync , а IdentityUser ( ApplicationUser ) войдет в приложение с SignInAsyncпомощью . Во время входа в систему UserManager можно хранить ApplicationUser утверждения для пользовательских данных, доступных на сайте Principal.
В примере приложения OnPostConfirmationAsync ( Account/ExternalLogin.cshtml.cs ) устанавливает утверждения языкового стандарта ( urn:google:locale ) и рисунка ( urn:google:picture ) для входа ApplicationUser , включая утверждение для GivenName:
public async Task OnPostConfirmationAsync(string returnUrl = null) < returnUrl = returnUrl ?? Url.Content("~/"); // Get the information about the user from the external login provider var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) < ErrorMessage = "Error loading external login information during confirmation."; return RedirectToPage("./Login", new < ReturnUrl = returnUrl >); > if (ModelState.IsValid) < var user = new IdentityUser < UserName = Input.Email, Email = Input.Email >; var result = await _userManager.CreateAsync(user); if (result.Succeeded) < result = await _userManager.AddLoginAsync(user, info); if (result.Succeeded) < // If they exist, add claims to the user for: // Given (first) name // Locale // Picture if (info.Principal.HasClaim(c =>c.Type == ClaimTypes.GivenName)) < await _userManager.AddClaimAsync(user, info.Principal.FindFirst(ClaimTypes.GivenName)); >if (info.Principal.HasClaim(c => c.Type == "urn:google:locale")) < await _userManager.AddClaimAsync(user, info.Principal.FindFirst("urn:google:locale")); >if (info.Principal.HasClaim(c => c.Type == "urn:google:picture")) < await _userManager.AddClaimAsync(user, info.Principal.FindFirst("urn:google:picture")); >// Include the access token in the properties var props = new AuthenticationProperties(); props.StoreTokens(info.AuthenticationTokens); props.IsPersistent = true; await _signInManager.SignInAsync(user, props); _logger.LogInformation( "User created an account using provider.", info.LoginProvider); return LocalRedirect(returnUrl); > > foreach (var error in result.Errors) < ModelState.AddModelError(string.Empty, error.Description); >> LoginProvider = info.LoginProvider; ReturnUrl = returnUrl; return Page(); >
По умолчанию утверждения пользователя хранятся в проверке подлинности cookie. Если проверка подлинности cookie слишком велика, это может привести к сбою приложения, так как:
- Браузер обнаруживает, что cookie заголовок слишком длинный.
- Общий размер запроса слишком велик.
Если для обработки запросов пользователей требуется большое количество данных пользователя:
- Ограничьте количество и размер утверждений пользователей для обработки запросов только тем, что требуется приложению.
- Используйте настраиваемый ITicketStore для по промежуточного Cookie слоя SessionStore проверки подлинности для хранения удостоверений между запросами. Сохраняйте большие объемы сведений об удостоверениях на сервере, отправляя клиенту только небольшой ключ идентификатора сеанса.
Сохранение маркера доступа
SaveTokens определяет, следует ли хранить маркеры доступа и обновления в AuthenticationProperties после успешной авторизации. SaveTokens По умолчанию устанавливается значение false , чтобы уменьшить размер окончательной проверки подлинности cookie.
Пример приложения задает значение SaveTokens true в GoogleOptions:
services.AddAuthentication().AddGoogle(options => < // Provide the Google Client ID options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com"; // Register with User Secrets using: // dotnet user-secrets set "Authentication:Google:ClientId" "" // Provide the Google Client Secret options.ClientSecret = ""; // Register with User Secrets using: // dotnet user-secrets set "Authentication:Google:ClientSecret" "" options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url"); options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string"); options.SaveTokens = true; options.Events.OnCreatingTicket = ctx => < Listtokens = ctx.Properties.GetTokens().ToList(); tokens.Add(new AuthenticationToken() < Name = "TicketCreated", Value = DateTime.UtcNow.ToString() >); ctx.Properties.StoreTokens(tokens); return Task.CompletedTask; >; >);
При OnPostConfirmationAsync выполнении сохраните маркер доступа (ExternalLoginInfo.AuthenticationTokens) из внешнего поставщика в ApplicationUser ‘s AuthenticationProperties .
Пример приложения сохраняет маркер доступа в OnPostConfirmationAsync (новая регистрация пользователя) и OnGetCallbackAsync (ранее зарегистрированный пользователь) в Account/ExternalLogin.cshtml.cs :
public async Task OnPostConfirmationAsync(string returnUrl = null) < returnUrl = returnUrl ?? Url.Content("~/"); // Get the information about the user from the external login provider var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) < ErrorMessage = "Error loading external login information during confirmation."; return RedirectToPage("./Login", new < ReturnUrl = returnUrl >); > if (ModelState.IsValid) < var user = new IdentityUser < UserName = Input.Email, Email = Input.Email >; var result = await _userManager.CreateAsync(user); if (result.Succeeded) < result = await _userManager.AddLoginAsync(user, info); if (result.Succeeded) < // If they exist, add claims to the user for: // Given (first) name // Locale // Picture if (info.Principal.HasClaim(c =>c.Type == ClaimTypes.GivenName)) < await _userManager.AddClaimAsync(user, info.Principal.FindFirst(ClaimTypes.GivenName)); >if (info.Principal.HasClaim(c => c.Type == "urn:google:locale")) < await _userManager.AddClaimAsync(user, info.Principal.FindFirst("urn:google:locale")); >if (info.Principal.HasClaim(c => c.Type == "urn:google:picture")) < await _userManager.AddClaimAsync(user, info.Principal.FindFirst("urn:google:picture")); >// Include the access token in the properties var props = new AuthenticationProperties(); props.StoreTokens(info.AuthenticationTokens); props.IsPersistent = true; await _signInManager.SignInAsync(user, props); _logger.LogInformation( "User created an account using provider.", info.LoginProvider); return LocalRedirect(returnUrl); > > foreach (var error in result.Errors) < ModelState.AddModelError(string.Empty, error.Description); >> LoginProvider = info.LoginProvider; ReturnUrl = returnUrl; return Page(); >
Добавление дополнительных пользовательских маркеров
Чтобы продемонстрировать, как добавить пользовательский маркер, который хранится в составе SaveTokens , пример приложения добавляет текущее DateTimeAuthenticationToken значение для AuthenticationToken.Name TicketCreated :
services.AddAuthentication().AddGoogle(options => < // Provide the Google Client ID options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com"; // Register with User Secrets using: // dotnet user-secrets set "Authentication:Google:ClientId" "" // Provide the Google Client Secret options.ClientSecret = ""; // Register with User Secrets using: // dotnet user-secrets set "Authentication:Google:ClientSecret" "" options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url"); options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string"); options.SaveTokens = true; options.Events.OnCreatingTicket = ctx => < Listtokens = ctx.Properties.GetTokens().ToList(); tokens.Add(new AuthenticationToken() < Name = "TicketCreated", Value = DateTime.UtcNow.ToString() >); ctx.Properties.StoreTokens(tokens); return Task.CompletedTask; >; >);
Создание и добавление утверждений
Платформа предоставляет общие действия и методы расширения для создания и добавления утверждений в коллекцию. Дополнительные сведения см. в разделах ClaimActionCollectionMapExtensions и ClaimActionCollectionUniqueExtensions.
Пользователи могут определять пользовательские действия, исходя из ClaimAction абстрактного Run метода и реализуя его.
Удаление действий и утверждений утверждений
ClaimActionCollection.Remove(String) удаляет все действия утверждения для заданной ClaimType коллекции. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) удаляет утверждение заданного ClaimType удостоверения. DeleteClaimв основном используется с openID Подключение (OIDC) для удаления утверждений, созданных протоколом.
Пример выходных данных приложения
User Claims http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier 9b342344f-7aab-43c2-1ac1-ba75912ca999 http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name someone@gmail.com AspNet.Identity.SecurityStamp 7D4312MOWRYYBFI1KXRPHGOSTBVWSFDE http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname Judy urn:google:locale en urn:google:picture https://lh4.googleusercontent.com/-XXXXXX/XXXXXX/XXXXXX/XXXXXX/photo.jpg Authentication Properties .Token.access_token yc23.AlvoZqz56. 1lxltXV7D-ZWP9 .Token.token_type Bearer .Token.expires_at 2019-04-11T22:14:51.0000000+00:00 .Token.TicketCreated 4/11/2019 9:14:52 PM .TokenNames access_token;token_type;expires_at;TicketCreated .persistent .issued Thu, 11 Apr 2019 20:51:06 GMT .expires Thu, 25 Apr 2019 20:51:06 GMT
Переадресация сведений запроса с помощью прокси-сервера или подсистемы балансировки нагрузки
Если приложение развертывается с прокси-сервером или подсистемой балансировки нагрузки, некоторые сведения из исходного запроса можно перенаправить в приложение в заголовках запроса. Эти сведения обычно включают безопасную схему запроса ( https ), узел и IP-адрес клиента. Приложения не считывают автоматически и не используют эти заголовки запроса.
Схема используется для создания ссылок, определяющих процесс проверки подлинности с помощью внешних поставщиков. Потеря безопасной схемы ( https ) приводит к тому, что приложение неправильно выполняет перенаправление на небезопасные URL-адреса.
Используйте ПО промежуточного слоя для перенаправленных заголовков, чтобы предоставить приложению сведения из исходных запросов для обработки запросов.
Дополнительные ресурсы
- dotnet/AspNetCore engineering SocialSample app: связанное приложение находится в конструкторской ветви репозитория main dotnet/AspNetCore GitHub. Ветвь main содержит код при активной разработке для следующего выпуска ASP.NET Core. Чтобы просмотреть версию примера приложения для выпущенной версии ASP.NET Core, используйте раскрывающийся список Branch , чтобы выбрать ветвь выпуска (например release/ ).
Идентификационный токен и токен доступа: в чем разница?
«Давайте воспользуемся токеном для защиты вызова API. Что мне использовать: ID-токен или Access токен? ID токен мне кажется предпочтительнее. В конце концов, если я знаю, кто является пользователем, я могу принимать более обоснованные решения об авторизации, верно?» Вам когда-нибудь приходилось приводить подобные аргументы? Выбор, основанный на интуиции, может оказаться хорошим, но то, что кажется интуитивным, не всегда верно. В случае ID и Access токенов, имеющих ясные и четко определенные цели, пользоваться ими нужно, исходя из этих соображений. Использование неправильного токена может привести к тому, что ваше решение будет небезопасным. «Что в конце концов изменится? Это всего лишь токены. Я могу использовать их по своему усмотрению. Что такого плохого может случиться?» Давайте подробнее рассмотрим эти два типа токенов, чтобы лучше понять их роль в процессах аутентификации и авторизации.
Что такое идентификационный токен?

ID токен — это артефакт, подтверждающий, что пользователь прошел аутентификацию. Он был представлен OpenID Connect (OIDC), открытым стандартом аутентификации, используемым многими системами идентификации такими, как Google, Facebook и, конечно же, Auth0. Ознакомьтесь с этим документом для получения дополнительных сведений об OpenID Connect. Давайте кратко рассмотрим проблему, которую призван решить OIDC. Рассмотрим следующую схему: Здесь пользователь со своим браузером аутентифицируется через провайдер OpenID и получает доступ к веб-приложению. Результатом процесса аутентификации на основе OpenID Connect является ID токен, который передается приложению в качестве доказательства того, что пользователь прошел аутентификацию. Подтверждение аутентификации пользователя это всего лишь базовое представление об ID токене. Рассмотрим это подробнее. Идентификационный токен JSON Web Token (JWT) — стандартный формат, который позволяет вашему приложению легко проверить его содержимое и убедиться, что оно исходит от ожидаемого эмитента и что никто другой его не менял. Если вы хотите узнать больше о JWT, посмотрите «The JWT Handbook». Проще говоря, пример ID токена выглядит так: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vbXktZG9tYWluLmF1dGgwLmNvbSIsInN1YiI6
ImF1dGgwfDEyMzQ1NiIsImF1ZCI6IjEyMzRhYmNkZWYiLCJleHAiOjEzMTEyODE5NzAsImlhdCI6MTMxMTI4MDk3MCwibm
FtZSI6IkphbmUgRG 9lIiwiZ2l2ZW5fbmFtZSI6IkphbmUiLCJmYW1pbHlfbmFtZSI6IkRvZSJ9 Конечно, просто «на глазок» это не прочесть, поэтому вам нужно расшифровать его, чтобы увидеть, какой контент содержит JWT. Между прочим, ID токен не зашифрован, а закодирован только в Base 64. Вы можете использовать одну из множества доступных библиотек для его декодирования или самостоятельно проверить его с помощью сайта jwt.io. Не вдаваясь в подробности, соответствующая информация, содержащаяся в указанном выше ID токене, выглядит следующим образом: < "iss": "http://my-domain.auth0.com", "sub": "auth0|123456", "aud": "1234abcdef", "exp": 1311281970, "iat": 1311280970, "name": "Jane Doe", "given_name": "Jane", "family_name": "Doe" >Эти свойства JSON называются «claims (заявки)», и они представляют собой заявления о пользователе и самом токене. Заявки о пользователе позволяют определить его личность. Фактически, спецификации OpenID Connect не требуют, чтобы ID токен содержал заявку о пользователе. В своей минимальной структуре он содержит данные только для аутентификации. Одним из важных требований является «aud» заявка. Оно определяет получателя, то есть веб-приложение, которое должно быть конечным получателем токена. В случае с использованием ID токена, значением «aud» будет Client ID приложения, которое должно использовать токен. Запомните это небольшое уточнение об ограничении целевой информационной системы, поскольку оно поможет вам лучше понять, как в дальнейшем им правильно пользоваться. Идентификационный токен может содержать дополнительную информацию о пользователе: адрес его электронной почты, фотографию, день рождения и т. д. Наконец, возможно, самое важное: ID токен подписывает эмитент своим закрытым ключом. Для вас это гарантия происхождения токена и того, что он не был подделан. Это можно проверить, используя открытый ключ эмитента. Здорово! Теперь вы знаете, что такое идентификационный токен. Но что вы можете с ним делать? Во-первых, он демонстрирует, что пользователь был аутентифицирован организацией, которой вы доверяете (провайдер OpenID), и поэтому вы можете доверять его личным данным. Кроме того, ваше приложение может персонализировать взаимодействие с пользователями, используя данные о пользователе, включенные в ID токен. Например, вы можете показать имя в пользовательском интерфейсе или отобразить сообщение «С наилучшими пожеланиями» в день рождения. Самое интересное в том, что вам не нужно делать дополнительных запросов, поэтому вы можете немного повысить производительность своего приложения.
Что такое токен доступа?

Теперь, когда вы знаете, что такое идентификационный токен, давайте попробуем понять, что такое Access токен. Начнем с описания сценария, в который вписывается токен доступа: На схеме выше клиентское приложение хочет получить доступ к ресурсу, например API или чему-либо еще, что защищено от несанкционированного доступа. Два других элемента на этой диаграмме — это пользователь, который является владельцем ресурса, и сервер авторизации. В этом сценарии Access токен является артефактом, позволяющим клиентскому приложению получить доступ к ресурсу пользователя. Он выдается сервером авторизации после успешной аутентификации пользователя и получения его согласия. В контексте OAuth 2 Access токен позволяет клиентскому приложению получать доступ к определенному ресурсу для выполнения определенных действий от имени пользователя. Что известно как сценарий делегированной авторизации: пользователь делегирует клиентскому приложению доступ к ресурсу от своего имени. Это значит, например, что вы можете от своего имени разрешить приложению LinkedIn доступ к API Twitter для перекрестной публикации на обеих социальных платформах. Заметьте, что вы разрешаете LinkedIn только публиковать свои сообщения в Twitter. Вы не разрешаете ему удалять их, изменять данные вашего профиля или делать что-то еще. Это ограничение очень важно в сценарии делегированной авторизации и достигается с помощью областей действия, или scopes. Области действия — это механизм, позволяющий пользователю авторизовать стороннее приложение для выполнения только определенных операций. Конечно, API, получающий Access токен, должен получить подтверждение, что это в самом деле действительный токен, выпущенный сервером авторизации, которому он доверяет, и принимать решения об авторизации на основе связанной с ним информации. Другими словами, API должен каким-то образом использовать этот токен, чтобы авторизовать клиентское приложение для выполнения желаемой операции с ресурсом. Как следует использовать Access токен для принятия решений об авторизации, зависит от многих факторов: общей архитектуры системы, формата токена и т. д. Например, токен доступа может быть ключом, который позволяет API извлекать необходимую информацию из базы данных, совместно используемую с сервером авторизации, или может напрямую содержать необходимую информацию в закодированном формате. Это значит, что понимание того, как получить необходимую информацию для принятия решений об авторизации, является соглашением между сервером авторизации и сервером ресурсов, то есть API. В спецификации OAuth 2 ничего не говорится о формате Access токена. Это может быть строка в любом формате. Распространенным форматом, используемым для токенов доступа, является JWT, и на данный момент этот стандарт в разработке. Однако это не значит, что Access токены должны быть именно в этом формате. Хорошо! Теперь вы знаете, что такое ID-токен и Access токен. Итак, вы готовы использовать их, не боясь ошибиться. Но подождите. Мне кажется, вы не уверены. Возможно, вам нужна другая информация. Ok. Итак, давайте посмотрим, для чего эти токены не подходят.
Для чего НЕ подходит ID-токен?
Одна из наиболее распространенных ошибок, допускаемых разработчиками при использовании идентификационного токена — это использование его для вызова API. Как сказано выше, ID токен доказывает, что пользователь прошел аутентификацию. В собственном сценарии, то есть в сценарии, в котором клиент и API контролируются вами, вы можете решить, что ваш идентификационный токен подходит для принятия решений об авторизации и, возможно, все, что вам нужно знать — это личность пользователя. Однако даже в этом сценарии безопасность вашего приложения, состоящего из клиента и API, может быть под угрозой. Фактически, не существует механизма, который привязывает ID токен к каналу клиентского API. Если злоумышленнику удастся украсть вашу «личность», он может использовать ее для вызова вашего API как законного клиента. С другой стороны, для Acces токена существует набор методов, известных как ограничение отправителя (sender constraint), которые позволяют привязать токен доступа к определенному отправителю. Это гарантия того, что даже если злоумышленник украдет Access токен, он не сможет использовать его для доступа к вашему API, поскольку токен привязан к клиенту, который изначально его запросил. В сценарии делегированной авторизации, когда сторонний клиент хочет вызвать ваш API, нельзя использовать ID токен для вызова API. Помимо отсутствия механизмов привязки к клиенту есть несколько других причин, по которым этого не следует делать. Если ваш API принимает ID токен в качестве токена авторизации, вы сначала игнорируете предполагаемого получателя, указанного в aud (audience claim). В этом утверждении говорится, что он предназначен для вашего клиентского приложения, а не для сервера ресурсов (т. е. API). Можно подумать, что это всего лишь формальность, но такой подход может повлечь за собой риски для безопасности. Прежде всего, кроме прочих проверок, ваш API не должен принимать токен, который для него не предназначен. Если это произойдет, его безопасность окажется под угрозой. Фактически, если вашему API все равно, предназначен ли для него токен, для доступа к нему может быть использован ID токен, украденный из любого клиентского приложения, может быть использован для доступа к вашему API. Разумеется, для предотвращения несанкционированного доступа, проверка aud — это всего лишь одна из проверок, которую должен выполнять ваш API. Кроме того, вашему идентификатору не будут предоставлены области действия (да, это еще одна боль). Как было сказано ранее, области позволяют пользователю ограничивать операции, которые ваше клиентское приложение может выполнять от их имени. Эти области связаны с Access токеном, чтобы ваш API знал, что может делать клиентское приложение, а что нет. Если ваше клиентское приложение использует ID токен для вызова API, вы игнорируете эту функцию и потенциально разрешаете приложению выполнять действия, не авторизованные пользователем.
Для чего НЕ подходит Access токен?
Что касается Access токена, то он был задуман, чтобы продемонстрировать, что вы авторизованы для доступа к ресурсу, например, для вызова API. Ваше клиентское приложение должно использовать его только с этой целью. Другими словами, Access токен не должен проверяться клиентским приложением. Он предназначен для сервера ресурсов, и клиентское приложение должно обрабатывать Access токены как непрозрачные строки, то есть строки без определенного значения. Даже если вам известен формат Access токена, не следует пытаться интерпретировать его содержимое в клиентском приложении. Как уже говорилось, формат Access токена — это соглашение между сервером авторизации и сервером ресурсов, и клиентское приложение не должно вмешиваться. Подумайте, что может случиться, если однажды формат Access токена изменится. Если ваш клиентский код (client code) проверял этот Access токен, в этом случае он будет тотчас сломан.
Краткий обзор
Путаница с использованием ID и Access токенов очень распространена, и, возможно, трудно понять различия между ними. Может быть, это в основном связано с отсутствием четкого понимания различных целей каждого артефакта, определенных в спецификациях OAuth и OpenID Connect. Кроме того, понимание сценариев, в которых изначально должны были действовать эти артефакты, играет важную роль в предотвращении путаницы при их использовании. Тем не менее, я надеюсь, что теперь эта тема немного прояснилась. В иллюстрации кратко изложено то, что можно и чего нельзя делать с ID и Access токенами:
Переведено со статьи, полный текст доступен по ссылке.
Security Token Handler. Validate Token(SecurityToken) Метод
Некоторые сведения относятся к предварительной версии продукта, в которую до выпуска могут быть внесены существенные изменения. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.
При переопределении в производном классе проверяет указанный токен безопасности. Токен должен иметь тип, обрабатываемый производным классом.
public: virtual System::Collections::ObjectModel::ReadOnlyCollection ^ ValidateToken(System::IdentityModel::Tokens::SecurityToken ^ token);
public virtual System.Collections.ObjectModel.ReadOnlyCollection ValidateToken (System.IdentityModel.Tokens.SecurityToken token);
abstract member ValidateToken : System.IdentityModel.Tokens.SecurityToken -> System.Collections.ObjectModel.ReadOnlyCollection override this.ValidateToken : System.IdentityModel.Tokens.SecurityToken -> System.Collections.ObjectModel.ReadOnlyCollection
Public Overridable Function ValidateToken (token As SecurityToken) As ReadOnlyCollection(Of ClaimsIdentity)
Параметры
Токен для проверки.
Возвращаемое значение
Идентификаторы, содержащееся в токене.
Примеры
В следующем коде показано переопределение ValidateToken метода для обработчика маркеров безопасности, который обрабатывает простые веб-маркеры (SWT). Код взят из CustomToken примера. Сведения об этом и других примерах, доступных для WIF, а также о том, где их можно скачать, см. в разделе WiF Code Sample Index.
/// /// This method validates the Simple Web Token. /// /// A simple web token. /// A Claims Collection which contains all the claims from the token. public override ReadOnlyCollection ValidateToken(SecurityToken token) < if ( token == null ) < throw new ArgumentNullException( "token" ); >SimpleWebToken simpleWebToken = token as SimpleWebToken; if ( simpleWebToken == null ) < throw new ArgumentException("The token provided must be of type SimpleWebToken."); >if ( DateTime.Compare( simpleWebToken.ValidTo.Add( Configuration.MaxClockSkew ), DateTime.UtcNow ) ValidateSignature( simpleWebToken ); ValidateAudience( simpleWebToken.Audience ); ClaimsIdentity claimsIdentity = CreateClaims( simpleWebToken ); if (this.Configuration.SaveBootstrapContext) < claimsIdentity.BootstrapContext = new BootstrapContext(simpleWebToken.SerializedToken); >List claimCollection = new List(new ClaimsIdentity[] < claimsIdentity >); return claimCollection.AsReadOnly(); >
В следующем коде показан CreateClaims метод, который вызывается из переопределения ValidateToken метода в предыдущем примере. Этот метод возвращает ClaimsIdentity объект, созданный из утверждений в маркере. Код взят из CustomToken примера. Сведения об этом и других примерах, доступных для WIF, а также о том, где их можно скачать, см. в разделе WiF Code Sample Index.
/// Creates 's from the incoming token. /// /// The incoming . /// A created from the token. protected virtual ClaimsIdentity CreateClaims( SimpleWebToken simpleWebToken ) < if ( simpleWebToken == null ) < throw new ArgumentNullException( "simpleWebToken" ); >NameValueCollection tokenProperties = simpleWebToken.GetAllProperties(); if ( tokenProperties == null ) < throw new SecurityTokenValidationException( "No claims can be created from this Simple Web Token." ); >if ( Configuration.IssuerNameRegistry == null ) < throw new InvalidOperationException( "The Configuration.IssuerNameRegistry property of this SecurityTokenHandler is set to null. Tokens cannot be validated in this state." ); >string normalizedIssuer = Configuration.IssuerNameRegistry.GetIssuerName( simpleWebToken ); ClaimsIdentity identity = new ClaimsIdentity(AuthenticationTypes.Federation); foreach ( string key in tokenProperties.Keys ) < if ( ! IsReservedKeyName(key) && !string.IsNullOrEmpty( tokenProperties[key] ) ) < identity.AddClaim( new Claim( key, tokenProperties[key], ClaimValueTypes.String, normalizedIssuer ) ); if ( key == AcsNameClaimType ) < // add a default name claim from the Name identifier claim. identity.AddClaim( new Claim( DefaultNameClaimType, tokenProperties[key], ClaimValueTypes.String, normalizedIssuer ) ); >> > return identity; >
В следующем коде показан ValidateSignature метод, который вызывается из переопределения ValidateToken метода в простом обработчике веб-маркера. Этот метод проверяет подпись маркера с помощью настроенного IssuerTokenResolver. Код взят из CustomToken примера. Сведения об этом и других примерах, доступных для WIF, а также о том, где их можно скачать, см. в разделе WiF Code Sample Index.
/// /// Validates the signature on the incoming token. /// /// The incoming . protected virtual void ValidateSignature( SimpleWebToken simpleWebToken ) < if ( simpleWebToken == null ) < throw new ArgumentNullException( "simpleWebToken" ); >if ( String.IsNullOrEmpty( simpleWebToken.SerializedToken ) || String.IsNullOrEmpty( simpleWebToken.Signature ) ) < throw new SecurityTokenValidationException( "The token does not have a signature to verify" ); >string serializedToken = simpleWebToken.SerializedToken; string unsignedToken = null; // Find the last parameter. The signature must be last per SWT specification. int lastSeparator = serializedToken.LastIndexOf( ParameterSeparator ); // Check whether the last parameter is an hmac. if ( lastSeparator > 0 ) < string lastParamStart = ParameterSeparator + SimpleWebTokenConstants.Signature + "="; string lastParam = serializedToken.Substring( lastSeparator ); // Strip the trailing hmac to obtain the original unsigned string for later hmac verification. if ( lastParam.StartsWith( lastParamStart, StringComparison.Ordinal ) ) < unsignedToken = serializedToken.Substring( 0, lastSeparator ); >> SimpleWebTokenKeyIdentifierClause clause = new SimpleWebTokenKeyIdentifierClause(simpleWebToken.Audience); InMemorySymmetricSecurityKey securityKey = null; try < securityKey = (InMemorySymmetricSecurityKey)this.Configuration.IssuerTokenResolver.ResolveSecurityKey(clause); >catch (InvalidOperationException) < throw new SecurityTokenValidationException( "A Symmetric key was not found for the given key identifier clause."); >string generatedSignature = GenerateSignature( unsignedToken, securityKey.GetSymmetricKey() ); if ( string.CompareOrdinal( generatedSignature, simpleWebToken.Signature ) != 0 ) < throw new SecurityTokenValidationException( "The signature on the incoming token is invalid.") ; >>
/// /// Generates an HMACSHA256 signature for a given string and key. /// /// The token to be signed. /// The key used to generate the signature. /// The generated signature. protected static string GenerateSignature(string unsignedToken, byte[] signingKey) < using (HMACSHA256 hmac = new HMACSHA256(signingKey)) < byte[] signatureBytes = hmac.ComputeHash(Encoding.ASCII.GetBytes(unsignedToken)); string signature = HttpUtility.UrlEncode(Convert.ToBase64String(signatureBytes)); return signature; >>
В следующем коде показан ValidateAudience метод, который вызывается из переопределения ValidateToken метода в простом обработчике веб-маркера. Этот метод проверяет аудиторию, содержащуюся в маркере, по URI аудитории, указанной в конфигурации. Код взят из CustomToken примера. Сведения об этом и других примерах, доступных для WIF, а также о том, где их можно скачать, см. в разделе WiF Code Sample Index.
/// /// Validates the audience of the incoming token with those specified in configuration. /// /// The audience of the incoming token. protected virtual void ValidateAudience( string tokenAudience ) < if ( Configuration.AudienceRestriction.AudienceMode != AudienceUriMode.Never ) < if ( String.IsNullOrEmpty( tokenAudience ) ) < throw new SecurityTokenValidationException("The incoming token does not have a valid audience Uri and the Audience Restriction is not set to 'None'."); >if ( Configuration.AudienceRestriction.AllowedAudienceUris.Count == 0 ) < throw new InvalidOperationException( " Audience Restriction is not set to 'None' but no valid audience URI's are configured." ); >IList allowedAudienceUris = Configuration.AudienceRestriction.AllowedAudienceUris; Uri audienceUri = null; Uri.TryCreate(tokenAudience, UriKind.RelativeOrAbsolute, out audienceUri); // Strip off any query string or fragment. Uri audienceLeftPart; if ( audienceUri.IsAbsoluteUri ) < audienceLeftPart = new Uri( audienceUri.GetLeftPart( UriPartial.Path ) ); >else < Uri baseUri = new Uri( "http://www.example.com" ); Uri resolved = new Uri( baseUri, tokenAudience ); audienceLeftPart = baseUri.MakeRelativeUri( new Uri( resolved.GetLeftPart( UriPartial.Path ) ) ); >if ( !allowedAudienceUris.Contains( audienceLeftPart ) ) < throw new AudienceUriValidationFailedException( "The Audience Uri of the incoming token is not present in the list of permitted Audience Uri's."); >> >
Комментарии
По умолчанию этот метод создает NotImplementedException исключение.
Метод ValidateToken вызывается инфраструктурой для проверки и извлечения утверждений из десериализованного маркера безопасности. Эти утверждения возвращаются в коллекции объектов, ClaimsIdentity возвращаемых методом . В типичном случае эта коллекция будет содержать одно удостоверение.
В производных классах проверка обычно включает проверку целевой аудитории, указанной в токене, по URI аудитории, указанной в SecurityTokenHandlerConfiguration.AudienceRestriction свойстве объекта конфигурации обработчика маркеров, указанного в свойстве Configuration . Эти URI обычно задаются в файле конфигурации в элементе . Если аудитория не может быть проверена, AudienceUriValidationFailedException должно быть вызвано исключение.
При обработке маркера издатель обычно проверяется путем передачи маркера издателя в один из GetIssuerName методов IssuerNameRegistry объекта, настроенного для обработчика с помощью Configuration свойства . Реестр имен издателя обычно настраивается с помощью в файле конфигурации. Возвращает GetIssuerName имя издателя. Это имя следует использовать для задания Claim.Issuer свойства в утверждениях, содержащихся в маркере. Если реестр имен издателя не содержит запись для токена издателя, GetIssuerName возвращает . null В этом случае SecurityTokenException обычно создается в производных классах, но это поведение определяется конструктором класса .