Использование Identity Server 4 в Net Core 3.0

Введение

На одном из моих поддерживаемых проектов недавно встала задача проанализировать возможность миграции с .NET фреймворка 4.5 на .Net Core по случаю необходимости рефакторинга и разгребания большого количества накопившегося технического долга. Выбор пал на целевую платформу .NET Core 3.0, так как, судя по утверждению разработчиков от Microsoft, с появлением релиза версии 3.0, необходимые шаги при миграции legacy кода уменьшатся в несколько раз. Особенно нас в нем привлекли планы выхода EntityFramework 6.3 для .Net Core т.е. большую часть кода, основанную на EF 6.2, можно будет оставить «как есть» в мигрированном проекте на net core.

С уровнем данных, вроде, стало понятно, однако, еще одной большой частью по переносу кода остался уровень безопасности, который, к сожалению, после беглых выводов аудита придется почти полностью выкинуть и переписать с нуля. Благо, на проекте уже использовалась часть ASP NET Identity, в виде хранения пользователей и других приделанных сбоку «велосипедов».

Тут возникает логичный вопрос: если в security часть придется вносить много изменений, почему бы сразу же не внедрить подходы, рекомендуемые в виде промышленных стандартов, а именно: подвести приложение под использование Open Id connect и OAuth посредством фреймворка IdentityServer4.


Проблемы и пути решения

Итак, нам дано: имеется JavaScript приложение на Angular (Client в терминах IS4), оно использует некоторое подмножество WebAPI (Resources), также есть база данных устаревшего ASP NET Identity с логинами пользователей, которые необходимо после обновления использовать заново (чтобы не заводить всех еще раз), плюс в некоторых случаях необходимо давать возможность входить в систему через Windows аутентификацию на стороне IdentityServer4. Т.е. бывают случаи, когда пользователи работают через локальную сеть в домене ActiveDirectory.

Основное решение миграции данных о пользователях состоит в том, чтобы вручную (или с помощью автоматизированных средств) написать скрипт миграции между старой и новой схемой данных Identity. Мы, в свою очередь, воспользовались автоматическим приложением сравнения схем данных и сгенерировали SQL скрипт, в зависимости от версии Identity целевой миграционный скрипт будет содержать разные инструкции по обновлению. Тут главное- не забыть согласовать таблицу EFMigrationsHistory, если до этого использовался EF и в дальнейшем планируется, например, расширять сущность IdentityUser на дополнительные поля.

А вот как правильно теперь сконфигурировать IdentityServer4 и настроить его совместно с Windows учетными записями будет описано ниже.


План реализации

По причинам NDA я не стану описывать, как мы добились внедрения IS4 у себя на проекте, однако, в данной статье я на простом сайте ASP.NET Core, созданном с нуля, покажу, какие шаги нужно предпринять, чтобы получить полностью сконфигурированное и работоспособное приложение, использующее для целей авторизации и аутентификации IdentityServer4.
Чтобы реализовать желаемое поведение нам предстоит совершить следующие шаги:


  • Создать пустой проект ASP.Net Core и сконфигурировать на использование IdentityServer4.
  • Добавить клиента в виде Angular приложения.
  • Реализовать вход через open-id-connect google
  • Добавить возможность выбора Windows аутентификации

По соображениям краткости все три компонента (IdentityServer, WebAPI, Angular клиент) будут находиться в одном проекте. Выбранный тип взаимодействия клиента и IdentityServer (GrantType) — Implicit flow, когда access_token передается на сторону приложения в браузере, а затем используется при взаимодействии с WebAPI. Ближе к релизу, судя по изменениям в репозитории ASP.NET Core, Implicit flow будет заменена на Authorization Code + PKCE.)

В процессе создания и изменения приложения будет широко применяться интерфейс командной строки .NET Core, он должен быть установлен в системе в месте с последней версией preview Core 3.0 (на момент написание статьи 3.0.100-preview7–012821).


Создание и конфигурирование web проекта

Выход IdentityServer версии 4, ознаменовался полным выпиливанием UI с этого фреймворка. Теперь у разработчиков появилось полное право самим определять главный интерфейс сервера авторизации. Тут есть несколько способов. Одним из популярных является использование UI из пакета QuickStart UI, его можно найти в официальном репозитории на github.

Другим, не менее удобным, способом является интеграция с ASP NET Core Identity UI, в данном случае разработчику необходимо правильно сконфигурировать соответствующие промежуточные ПО в проекте. Именно данный способ и будет описываться далее.

Начнем с создания простого web проекта, для этого выполним в командной строке следующую инструкцию:

dotnet new webapp -n IdentityServer4WebApp

После исполнения на выходе у нас будет каркас вэб приложения, который постепенно будет доводиться до нужного нам состояния. Тут нужно сделать оговорку, что в .Net Core 3.0 для Identity используются более легковесные RazorPages, в отличии от тяжеловесного MVC.
Теперь необходимо добавить поддержку IdentityServer в наш проект. Для этого устанавливаем необходимые пакеты:

dotnet add package Microsoft.AspNetCore.ApiAuthorization.IdentityServer -v 3.0.0-preview7.19365.7
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore -v 3.0.0-preview7.19365.7
dotnet add package Microsoft.EntityFrameworkCore.Tools -v 3.0.0-preview7.19362.6
dotnet add package Microsoft.EntityFrameworkCore.Sqlite -v 3.0.0-preview7.19362.6

Помимо ссылок на пакеты сервера авторизации, здесь мы добавили поддержку Entity Framework для хранения информации о пользователях в экосистеме Identity. Для простоты будем использовать базу SQLite.

Для инициализации базы создадим модель нашего пользователя и контекст базы данных, для этого объявим два класса ApplicationUser, наследуемый от IdentityUser в папке Models и ApplicationDbContext, наследуемый от: ApiAuthorizationDbContext в папке Data.

Далее необходимо сконфигурировать использование контекста EntityFramework и создать базу данных. Для этого прописываем контекст в метод ConfigureServices класса Startup:

public void ConfigureServices(IServiceCollection services)
{
  services.AddDbContext(options =>options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
    services.AddRazorPages();
}

И добавляем строку подключения в appsettings.json

"ConnectionStrings": {
    "DefaultConnection": "Data Source=data.db"
  },

Теперь можно создать первоначальную миграцию и проинициализировать схему базы данных. Тут стоит заметить, что необходим установленный tool для ef core (для рассматриваемого preview нужна версия 3.0.0-preview7.19362.6).

dotnet ef migrations add Init
dotnet ef database update

Если все предыдущие шаги были выполнены без ошибок, в вашем проекте должен появиться SQLite файл данных data.db.

На данном этапе мы уже можем полностью сконфигурировать и опробовать полноценную возможность использования Asp.Net Core Identity. Для этого внесем изменения в методы Startup. Configure и Startup.ConfigureServices.

//Startup.ConfigureServices:
services.AddDefaultIdentity()
                .AddEntityFrameworkStores();

//Startup. Configure:
app.UseAuthentication();
app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
    endpoints.MapRazorPages();
});

Этими строчками мы встраиваем возможность аутентификации и авторизации в конвейер обработки запросов. А также добавляем дефолтный пользовательский интерфейс для Identity.
Осталось только подправить UI, добавим в Pages\Shared новое представление Razor view с именем _LoginPartial.cshtml и следующим содержимым:

@using IdentityServer4WebApp.Models
@using Microsoft.AspNetCore.Identity
@inject SignInManager SignInManager
@inject UserManager UserManager


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

Чтобы добиться рендеринга новых пунктов меню, просто изменим файл _Layout.cshtml, добавив туда рендеринг этого частичного представления.

                    
                

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

yla2yln6zflkftldetnacko8trm.png

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

В предпоследней строчке метода Startup.ConfigureServices добавить конфигурацию соглашений IS4 поверх ASP.NET Core Identity:

services.AddIdentityServer()
    .AddApiAuthorization();

Метод AddApiAuthorization указывает фреймворку использовать определенную поддерживаемую конфигурацию, главным образом через файл appsettings.json. На данный момент встроенные возможности по управлению IS4 не такие гибкие и следует относится к этому, как к некой отправной точке при построении своих приложений. В любом случае, можно воспользоваться перегруженной версией этого метода и более детально настроить параметры через callback.

​ Далее вызываем вспомогательный метод, который настраивает приложение для проверки токенов JWT, выданных фреймворком.

services.AddAuthentication()
    .AddIdentityServerJwt();

И наконец, в методе Startup.Configure добавить промежуточное ПО для
предоставления конечных точек Open ID Connect.

app.UseAuthentication();
app.UseAuthorization();
app.UseIdentityServer();//<-Сюда

Как говорилось выше по тексту, использованные вспомогательные методы читают конфигурацию в
файле настроек приложения appsettings.json, в которых мы должны добавить новую
секцию IdentityServer.

"IdentityServer": {
    "Clients": {
      "TestIdentityAngular": {
        "Profile": "IdentityServerSPA"
      }
    }
  }

В данной секции определяется клиента с именем TestIdentityAngular, которое мы присвоим будущему браузерному клиенту и определенным профилем конфигурации.

Профили приложений– это новое средство конфигурирования IdentityServer, предоставляющее несколько предопределенных конфигураций с возможностью уточнения некоторых параметров. Мы будем использовать профиль IdentityServerSPA, предназначенный для случаев, когда браузерный клиент и фреймворк расположены в одном проекте и имеют такие параметры:


Другие возможные профили — SPA (приложение без IS4), IdentityServerJwt (API размещенное совместно с IS4), API (отдельное API).

Помимо этого конфигурация регистрирует ресурсы:


Как известно, IdentityServer для подписи токенов использует сертификаты, их параметры также можно задать в конфигурационном файле, так на момент тестирования мы можем использовать
тестовый x509-сертификат, для этого нужно указать его в секции «Key» файла appsettings.Development.json.

"IdentityServer": {
    "Key": {
      "Type": "Development"
    }
  }

Теперь можно сказать, что бэкэнд, позволяющий использовать IdentityServer готов и можно приступать к реализации браузерного приложения.


Реализация клиента на Angular

Наше браузерное SPA будет написано на платформе Angular. Приложение будет содержать две страницы, одна — для неавторизированных пользователей, а другая для пользователей, прошедших проверку. В примерах используется версия 8.1.2

Для начала создадим будущий каркас:

ng new ClientApp

В процессе создания нужно ответить «да» на предложение использовать роутинг. И немного стилизируем страницу через библиотеку bootstrap:

cd ClientApp
ng add bootstrap

Дальше необходимо добавить поддержку хостинга SPA в наше основное приложение. Сперва нужно подправить проект csproj — добавить информацию о нашем браузерном приложении.


    netcoreapp3.0

    true
    Latest
    false
    ClientApp\
    $(DefaultItemExcludes);$(SpaRoot)node_modules\**
    false
  
    
    
    
  


    
      
    
    
    
    
  

После этого устанавливаем специальный пакет nuget для поддержки браузерных приложений.

dotnet add package Microsoft.AspNetCore.SpaServices.Extensions -v 3.0.0-preview7.19365.7

И применяем его вспомогательные методы:

//Startup. ConfigureServices:
services.AddSpaStaticFiles(configuration =>
            {
                configuration.RootPath = "ClientApp/dist";
            });

//Startup. Configure:
app.UseSpa(spa =>
            {
                spa.Options.SourcePath = "ClientApp";

                if (env.IsDevelopment())
                {
                    spa.UseAngularCliServer(npmScript: "start");
                }
            });

Помимо вызова новых методов необходимо удалить страницы Razor Index.chtml и _ViewStart.chtml, чтобы контент теперь предоставляли сервисы SPA.

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

mvlvj6_2aqcutz9t0_9eozhoxk8.png

Теперь необходимо настроить роутинг, для этого добавляем в проект 2
компонента:

ng generate component Home -t=true -s=true --skipTests=true
ng generate component Data -t=true -s=true --skipTests=true

Прописываем их в таблице маршрутизации:

const routes: Routes = [
  { path: '', component: HomeComponent, pathMatch: 'full' },
  { path: 'data', component: DataComponent }
];

И изменяем файл app.component.html, чтобы правильно отобразить пункты меню.

Welcome to {{ title }}!

На этом шаге можно завершить основную подготовку каркаса приложения для внедрения взаимодействия через токены, выданные IdentityServer.

Текущий этап подготовки каркаса нашего SPA можно назвать завершенным и теперь следует приступить к реализации модуля, отвечающего за взаимодействие с серверной частью по протоколам OpenID Connect и OAuth. К счастью, разработчики от Microsoft уже реализовали такой код и теперь можно просто позаимствовать этот модуль у них. Так как моя статья пишется, основываясь на предрелизе 7 ASP.NET Core 3.0, весь код мы будем брать по релизной метке «v3.0.0-preview7.19365.7» на github.

Перед импортом кода необходимо установить библиотеку oidc-client, которая
предоставляет множество интерфейсов для браузерных приложений, а также
поддерживает управление пользовательскими сессиями и токенами доступа. Для
начала работы с ней нужно установить соответствующий пакет.

npm install oidc-client@1.8.0

Теперь в наше SPA необходимо внедрить модуль, инкапсулирующий полное взаимодействие по требуемым протоколам. Для этого нужно взять полностью модуль ApiAuthorizationModule из вышеуказанной метки репозитория ASP.NET Core и добавить в приложение все его файлы.

Помимо этого, необходимо импортировать его в главный модуль приложения AppModule:

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    DataComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    ApiAuthorizationModule,//<-Сюда
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Для отображения новых пунктов меню в импортированном модуле есть компонент app-login-menu,
его можно полностью изменить в соответствии с вашими потребностями и добавить
ссылку на него в секцию навигации представления app.component.html.

Модуль авторизации API для конфигурирования OpenID connect клиента SPA должен использовать специальный endpoint в бэке приложения, для его реализации мы
должны выполнить такие шаги:


  1. Исправить ID клиента в соответствии с тем, что мы задали в конфигурационном файле appsettings.json в секции IdentityServer: Clients, в нашем случае это TestIdentityAngular, прописывается оно в первой строчке набора констант api-authorization.constants.ts.
  2. Добавить контроллер OidcConfigurationController, который будет непосредственно возвращать конфигурацию в браузерное приложение

Код создаваемого контроллера представлен ниже:

    [ApiController]
    public class OidcConfigurationController: ControllerBase
    {
        private readonly IClientRequestParametersProvider _clientRequestParametersProvider;

        public OidcConfigurationController(IClientRequestParametersProvider clientRequestParametersProvider)
        {
            _clientRequestParametersProvider = clientRequestParametersProvider;
        }

        [HttpGet("_configuration/{clientId}")]
        public IActionResult GetClientRequestParameters([FromRoute]string clientId)
        {
            var parameters = _clientRequestParametersProvider.GetClientParameters(HttpContext, clientId);
            return Ok(parameters);
        }
    }

Также нужно сконфигурировать поддержку API точек для приложения бэка.

//Startup.ConfigureServices:
services.AddControllers();//<- Сюда
services.AddRazorPages();

//Startup. Configure:
app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
                endpoints.MapControllers();
            });

Теперь настало время запустить приложение. На главной странице в верхнем меню должны появиться два пункта — Login и Register. Также при старте импортированный модуль авторизации запросит у серверной стороны конфигурацию клиента, которую впоследствии и будет учитывать в протоколе. Примерный вывод конфигурации показан ниже:

{
    "authority": "https://localhost:44367",
    "client_id": "TestIdentityAngular",
    "redirect_uri": "https://localhost:44367/authentication/login-callback",
    "post_logout_redirect_uri": "https://localhost:44367/authentication/logout-callback",
    "response_type": "id_token token",
    "scope": "IdentityServer4WebAppAPI openid profile"
}

Как видно, клиент при взаимодействии ожидает получить id токен и токен доступа, а также он сконфигурирован на область доступа к нашему API.

Теперь если же мы выберем пункт меню Login, нас должно редиректнуть на страницу нашего IdentityServer4 и тут мы можем ввести логин и пароль, и если они корректны, мы сразу будем перекинуты назад в браузерное приложение, которое в свою очередь получит id_token и access_token. Как видно ниже, компонент app-login-menu сам определил, что авторизация успешно завершилась, и отобразил «приветствие», а также кнопку для Logout.

crl-aa3ehwgdjybwkmb7nbg92pk.png

При открытии в браузере «средств разработчика», можно увидеть в backstage все взаимодействие по протоколу OIDC/OAuth. Это получение информации об авторизующем сервере
через endpoint .well-known/openid-configuration и pooling активности сессии через точку доступа connect/checksession. Помимо этого, модуль авторизации настроен на механизм «тихого обновления токенов», когда при истекании времени действия токена доступа, система самостоятельно проходит шаги авторизации в скрытом iframe. Отключить автообновления токенов можно задав значение параметра includeIdTokenInSilentRenew равным «false» в файле authorize.service.ts.

Теперь можно заняться ограничением доступа неавторизированным пользователям со стороны компонентов SPA приложения, а также некоторым API контроллерам на бэке. В целях демонстрации некоторого API создадим в папке Models класс ExchangeRateItem, , а так же контроллер в папке Controller, возвращающий некоторые случайные данные.

//Controller:
    [ApiController]
    public class ExchangeRateController
    {
        private static readonly string[] Currencies = new[]
        {
            "EUR", "USD", "BGN", "AUD", "CNY", "TWD", "NZD", "TND", "UAH", "UYU", "MAD"
        };

        [HttpGet("api/rates")]
        public IEnumerable Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new ExchangeRateItem
            {
                FromCurrency = "RUR",
                ToCurrency = Currencies[rng.Next(Currencies.Length)],
                Value = Math.Round(1.0+ 1.0/rng.Next(1, 100),2)
                })
                .ToArray();
        }
    }

//Models:
public class ExchangeRateItem
    {
        public string FromCurrency { get; set; }

        public string ToCurrency { get; set; }

        public double Value { get; set; }
    }

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

ng generate component ExchangeRate -t=true -s=true --skipTests=true

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


Код
import { Component, OnInit, Input } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of, Subject } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Component({
  selector: 'app-exchange-rate',
  template: `
{{msg}}
From currency To currency Rate
{{ rate.fromCurrency }} {{ rate.toCurrency }} {{ rate.value }}
`, styles: [] }) export class ExchangeRateComponent implements OnInit { public rates: Observable; public errorMessage: Subject; @Input() public apiUrl: string; constructor(private http: HttpClient) { this.errorMessage = new Subject(); } ngOnInit() { this.rates = this.http.get("/api/"+this.apiUrl).pipe(catchError(this.handleError(this.errorMessage)) ); } private handleError(subject: Subject): (te:any) => Observable { return (error) => { let message = ''; if (error.error instanceof ErrorEvent) { message = `Error: ${error.error.message}`; } else { message = `Error Code: ${error.status}\nMessage: ${error.message}`; } subject.next(message); let emptyResult: ExchangeRateItem[] = []; return of(emptyResult); } } } interface ExchangeRateItem { fromCurrency: string; toCurrency: string; value: number; }

Теперь осталось начать использовать его на странице app-data, просто в template записав строку и можно снова запустить проект. Мы увидим при переходе по целевому пути, что компонент получил данные и вывел их в виде таблицы.

Далее попробуем добавить требование авторизации доступа к API контроллера, для этого у класса ExchangeRateController добавим атрибут [Authorize] и еще раз запустим SPA, однако, после того, как мы перейдем снова на компонент, вызывающий наше API, мы увидим ошибку, что свидетельствует об отсутствующих заголовках авторизации.

fokvovvxneeouxy0k30wuvlwj4u.png

Для корректного добавления в исходящие запросы токена авторизации можно
задействовать Angular механизм перехватчиков Interceptors. К счастью, импортированный модуль уже содержит необходимый тип, нам нужно только зарегистрировать его в базовом модуле приложения.

providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthorizeInterceptor, multi: true }
  ],

После этих шагов все должно корректно отработать. Если снова посмотреть инструменты разработчика, в браузере будет виден новый заголовок авторизации Bearer access_token. На бэкенде данный токен будет провалидирован IdentityServer и он же даст разрешение на вызов защищенной точки API.

В окончание примера интеграции с сервером авторизации, можно на маршрут с данными по обменным курсам в SPA поставить Guard активации, он не даст пользователям переключиться на страницу, если они в данный момент не авторизованы. Данный защитник также представлен в импортированном ранее модуле, его нужно просто навесить на целевой маршрут.

{ path: 'data', component: DataComponent, canActivate: [AuthorizeGuard]  }

Теперь в случае, когда пользователь не залогинился в приложении и выбрал ссылку на наш защищенный компонент, его сразу перебросит на страницу авторизации с просьбой ввести логин и пароль. Итоговый код доступен на github.


Подключение внешнего входа в систему через поставщика Google

Для подключения входа через учетные записи Google для ASP.NET core 1.½.0+ существует отдельный Nuget пакет Microsoft.AspNetCore.Authentication.Google, однако, в связи с изменениями в политике самой корпорации, у Microsoft есть планы для ASP.NET Core 3.0+ признать его устаревшим. И теперь подключать рекомендуется через вспомогательный метод OpenIdConnectExtensions и AddOpenIdConnect, его мы и будем использовать в данной статье.

Устанавливаем расширение OpenIdConnect:

dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect -v 3.0.0-preview7.19365.7

Чтобы начать работу, нам нужно получить два ключевых значения от Google — Id Client и Client Secret, для этого предлагается выполнить следующие шаги:


Для хранения полученных ключей локально мы будем использовать Secret Manager. Чтобы сохраненные значения были доступны из приложения, необходимо выполнить следующие команды.

dotnet user-secrets init
dotnet user-secrets set "Authentication:Google:ClientId" "Должен быть ClientID"
dotnet user-secrets set "Authentication:Google:ClientSecret" "Должен быть ClientSecret"

Далее вызываем ранее указанный метод для добавления входа через поставщиков Google.

services.AddAuthentication()
                .AddOpenIdConnect("Google", "Google",
                    o =>
                    {
                        IConfigurationSection googleAuthNSection =
                            Configuration.GetSection("Authentication:Google");
                        o.ClientId = googleAuthNSection["ClientId"];
                        o.ClientSecret = googleAuthNSection["ClientSecret"];
                        o.Authority = "https://accounts.google.com";
                        o.ResponseType = OpenIdConnectResponseType.Code;
                        o.CallbackPath = "/signin-google"; 
                    })
                .AddIdentityServerJwt();

Теперь можем запустить проект и зайти на страницу логина, там должен появиться
вариант со входом через сервисы Google. Можно его выбрать и проверить, что все нормально работает, после авторизации пользователя перекидывает снова на SPA и там он может запросить данные со страницы курсов валют.

xirl5hobkgfaukb4wzp5sibosj4.png

Аналогичными способами, описанными выше, можно подключить других поставщиков OAuth аутентификации, причем добавить их в наше приложение одновременно. Полный список Nuget пакетов со вспомогательными методами можно найти по ссылке.


Доработка возможности входа под пользователями Windows

В некоторых случаях, когда SPA приложение предназначено для работы в интрасетях под управлением операционных систем Microsoft, может требоваться вход под учетными записями ActiveDirectory. В ситуации, если у нас выполняется серверный рендеринг Html в приложениях типа ASP.NET, WebForms и т.д., мы можем включить требование по Windows авторизации и работать в коде с типами WindowsIdentity, однако, с браузерными приложениями такой подход не сработает. Тут может нам помочь Identity Server, мы ему можем сказать чтобы он использовал пользователей Windows, как внешнего поставщика учетных записей и многие связанные с ними данные отображал в claims id_token и access_token. К счастью, разработчики IS4 уже нас снабдили примером, как можно добавить требуемую функциональность к разрабатываемым приложениям, этот пример можно
найти на github. Однако, его нужно будет адаптировать под наши нужды и связать с измененной инфраструктурой ASP.NET Core Identity 3.0.

В процессе внедрения нам необходимо доработать стандартный код шаблонов Identity, для этого затянем Razor шаблоны Login и ExternalLogin в наш проект (утилита CLI aspnet-codegenerator должна быть установлена глобально):

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design

dotnet aspnet-codegenerator identity -dc IdentityServer4WebApp.Data.ApplicationDbContext --files "Account.Login;Account.ExternalLogin"

После выполнения вышеуказанных инструкций, в проекте появится новая Area с именем Identity и несколькими файлами внутри, в некоторые из них в дальнейшем мы и внесем изменения.

Чтобы наш новый пункт отобразился в меню внешних поставщиков, мы должны переписать фильтр возможных схем авторизации. Дело в том, что Identity берет внешних поставщиков аутентификации из IAuthenticationSchemeProvider. GetAllSchemesAsync () по предикату DisplayName!= null, а вот у Windows поставщика свойство DisplayName = null. Для этого открываем модель LoginModel и в методе OnGetAsync заменяем следующий код:

ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
// <<Заменяем на>>
ExternalLogins =(await _schemeProvider.GetAllSchemesAsync()).Where(x => x.DisplayName != null ||(x.Name.Equals(IISDefaults.AuthenticationScheme,StringComparison.OrdinalIgnoreCase))).ToList();

Одновременно с этим внедряем через конструктор новое поле private readonly AuthenticationSchemeProvider _schemeProvider. Далее заходим во View страницы и изменяем логику отображения имен списка поставщиков Login.cshtml:


<<Заменяем на>>

И напоследок, включаем windows авторизацию при запуске в файле launchSettings.json
(при деплое на IIS, необходимо включить соответствующие настройки в файлах web.config).


"iisSettings": {
    "windowsAuthentication": true, 
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:15479",
      "sslPort": 44301
    }
  },

Теперь на странице авторизации можно увидеть кнопку «Windows» в списке внешних поставщиков.

e_6s-m8ot5h52dv6dgvid8bgpky.png

По причине наличия хостинга SPA приложения в одном проекте с IdentityServer анонимную аутентификацию нельзя полностью отключить. Поэтому воспользуемся некоторым «костылем» и заменим атрибут [AllowAnonymous] в классе страницы LoginModel на атрибут [Authorize (AuthenticationSchemes = «Windows»)], тем самым мы сможем гарантировать, что страница сможет работать с корректным WindowsIdentity.

Теперь доработаем страницу ExternalLogin, чтобы подсистема Identity смогла корректно обрабатывать Windows пользователей. Для начала создадим новый метод ProcessWindowsLoginAsync.


Новый метод
private async Task ProcessWindowsLoginAsync(string returnUrl)
        {
            var result = await HttpContext.AuthenticateAsync(IISDefaults.AuthenticationScheme);
            if (result?.Principal is WindowsPrincipal wp)
            {
                var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl });

                var props = _signInManager.ConfigureExternalAuthenticationProperties(IISDefaults.AuthenticationScheme, redirectUrl);
                props.Items["scheme"] = IISDefaults.AuthenticationScheme;

                var id = new ClaimsIdentity(IISDefaults.AuthenticationScheme);
                id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name));
                id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
                id.AddClaim(new Claim(ClaimTypes.NameIdentifier, wp.Identity.Name));

                var wi = wp.Identity as WindowsIdentity;
                var groups = wi.Groups.Translate(typeof(NTAccount));
                var hasUsersGroup = groups.Any(i => i.Value.Contains(@"BUILTIN\Users", StringComparison.OrdinalIgnoreCase));

                id.AddClaim(new Claim("hasUsersGroup", hasUsersGroup.ToString()));

                await HttpContext.SignInAsync(IdentityConstants.ExternalScheme, new ClaimsPrincipal(id), props);

                return Redirect(props.RedirectUri);
            }

            return Challenge(IISDefaults.AuthenticationScheme);
        }

Новый метод подготавливает необходимую информацию из данных, полученных от операционной системы к объектам и свойствам вручную созданного принципала внешних провайдеров.

Далее переделаем метод ExternalLoginModel.OnPost на асинхронный и добавим в начала на проверку целевого провайдера:

if (IISDefaults.AuthenticationScheme == provider)
            {
                return await ProcessWindowsLoginAsync(returnUrl);
            }

В процессе подготовки Claim для Windows пользователя мы использовали один нестандартный Claim «hasUsersGroup», чтобы он был доступен в токенах ID и access, необходимо его обработать отдельно. Для этого мы воспользуемся механизмами ASP.NET Identity и сохранением его в UserClaims. Начнем с написания вспомогательного метода в классе ExternalLoginModel.

private async Task UpdateClaims(ExternalLoginInfo info, ApplicationUser user, params string[] claimTypes)
        {
            if (claimTypes == null)
            {
                return;
            }

            var claimTypesHash = new HashSet(claimTypes);

            var claims = (await _userManager.GetClaimsAsync(user)).Where(c => claimTypesHash.Contains(c.Type)).ToList();

            await _userManager.RemoveClaimsAsync(user, claims);

            foreach (var claimType in claimTypes)
            {
                if (info.Principal.HasClaim(c => c.Type == claimType))
                {
                    claims = info.Principal.FindAll(claimType).ToList();
                    await _userManager.AddClaimsAsync(user, claims);
                }
            }
        }

Добавим вызов созданного кода в метод OnPostConfirmationAsync (который вызывается в момент
первого захода пользователя в систему).

result = await _userManager.AddLoginAsync(user, info);
                    if (result.Succeeded)
                    {
                        await _signInManager.SignInAsync(user, isPersistent: false);
                        _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);

                        await UpdateClaims(info, user, "hasUsersGroup");//Сюда

                        return LocalRedirect(returnUrl);
                    }

И в метод OnGetCallbackAsync, вызывающийся при последующих входах в систему.

var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor : true);
            if (result.Succeeded)
            {
                var user = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);

                await UpdateClaims(info, user, "hasUsersGroup");//Сюда

Теперь предположим, что мы хотим ввести к нашему методу WebAPI требование наличия
клейма «hasUsersGroup». Для этого определим новую политику «ShouldHasUsersGroup»

services.AddAuthorization(options =>
            {
                options.AddPolicy("ShouldHasUsersGroup", policy => { policy.RequireClaim("hasUsersGroup");});
            });

Далее в контроллере ExchangeRateController создадим новую точку подключения и обозначим
требование только что созданного Policy.

        [Authorize(Policy = "ShouldHasUsersGroup")]
        [HttpGet("api/internalrates")]
        public IEnumerable GetInternalRates()
        {
            return Get().Select(i=>{i.Value=Math.Round(i.Value-0.02,2);return i;});
        }

Создадим новый view для отображения скорректированных данных.

ng generate component InternalData -t=true -s=true --skipTests=true

Заменим в нем template и зарегистрируем в таблице маршрутизации и в представлении верхнего меню.

//internal-data.component.ts:
template: ` `,

//app-routing.module.ts:
{ path: ' internaldata', component: InternalDataComponent, canActivate: [AuthorizeGuard]  }

//app.component.html:

После вышеперечисленных шагов мы можем запустить наше приложение и перейти по новой
ссылке, однако, приложение вернет нам ошибку. Дело в том, что accsee_token на данный момент
не содержит claim с именем hasUsersGroup, чтобы это исправить все нестандартные клеймы
необходимо прописывать в конфигурацию ApiResources сервера авторизации. К сожалению, в момент написания статьи, такою настройку нельзя сделать декларативно через файл appsettings.json, и поэтому придется программным путем вносить необходимое изменение в методе Startup. ConfigureServices.

services.AddIdentityServer()
                .AddApiAuthorization(options =>
                {
                    var apiResource = options.ApiResources.First();
                    apiResource.UserClaims = new[] { "hasUsersGroup" };
                });

Теперь, если еще раз запустить приложение, заново зайти п

© Habrahabr.ru