
Предисловие
Сегодня в этой статье я хочу поделиться личным опытом работы и решением конкретного кейса. Работали над ним небольшой командой, но для простоты повествования буду писать от первого лица. Собственно сам кейс: есть Windows сервер с доменом и SSL сертификатом, на нём нужно поднять сервер авторизации с протоколом OpenId Connect и ещё два приложения, которые должны авторизовываться через сервер авторизации. И да, реализовать все нужно по максимуму использовав встроенный функционал.
“Сервер авторизации должен быть вынесен отдельно!” - скажете вы – Но в реальности крутим и вертим как можем. Звучит просто, как два пальца. Как было на деле сейчас расскажу.
Общая схема

Сервер авторизации
Тут все относительно просто, есть готовые примеры и описанная документация. Бери да делай! В качестве основы был использован пример у damienbod . И уже дальше доработан под себя как нужно.
YARP
YARP – обратный прокси сервер, относительно недавно выпущенный в релиз компанией Microsoft. В его настройке нет ничего сложного, есть описанная документация и примеры имплементации. Конкретно мы руководствовались вот этой статьей. Конфигурацию нашего прокси прикладываю ниже.
"ReverseProxy": { "Routes": { "minimumroute": { "ClusterId": "minimumcluster", "Match": { "Path": "{**catch-all}" } }, "route2": { "ClusterId": "Server", "Match": { "Path": "/Server/{*any}" }, "Transforms": [ { "PathRemovePrefix": "/Server" } ] }, "route3": { "ClusterId": "App1", "Match": { "Path": "/App1/{*any}" }, "Transforms": [ { "PathRemovePrefix": "/app1" } ] }, "route4": { "ClusterId": "App2", "Match": { "Path": "/App2/{*any}" }, "Transforms": [ { "PathRemovePrefix": "/app2" } ] } ] } }, "Clusters": { "minimumcluster": { "Destinations": { "first_destination": { "Address": "http://localhost:5001" } } }, "Server": { "Destinations": { "first_destination": { "Address": "http://localhost:5001" } } }, "App1": { "Destinations": { "first_destination": { "Address": "http://localhost:5002" } } }, "App2": { "Destinations": { "first_destination": { "Address": "http://localhost:5003" } } } } }
Вот и всё, готово?
Начинаем все запускать локально и неужели все работает – да? Ага, значит деплоим и вот незадача – не работает. В чем же ошибка?
Дело в том, что у нашего сервера авторизации адрес http:// localhost:5001, а у приложения http:// localhost:5002. Поэтому когда, все это развернуто на сервере происходит ошибка. Для решения этой проблемы нам нужно самим указать такой параметр как RedirectUri
options.Events.OnRedirectToIdentityProvider = ctx => { ctx.ProtocolMessage.RedirectUri = "https://domain/app1/signin-oidc"; return Task.CompletedTask; }; options.Events.OnRedirectToIdentityProviderForSignOut = ctx => { ctx.ProtocolMessage.PostLogoutRedirectUri = "https://domain/app1/signout-callback-oidc"; return Task.CompletedTask; };
Уточнение: signin-oidc и signout-callback-oidc – это встроенные методы, который устанавливается по умолчанию как конечные точки входа и выхода в пакете Microsoft.AspNetCore.Authentication.OpenIdConnect.
С одной проблемой разобрались, теперь редирект правильный. Но встречаем тут же следующую, которая звучит как Corellation failed. Заходим в панель разработчика и начинаем пристально изучать запросы и ответы. И находим предупреждение от браузера, что файлы cookie не установлены, так, как отсутствует атрибут secure. Путей решения этой проблемы два:
Установить для наших приложений самозаверяющиеся SSL сертификаты. Как это сделать описано здесь.
Либо на наших приложениях изменить конфигурацию политики файлов cookie. Для этого нам нужно прописать следующее в program.cs:
builder.Services.Configure<CookiePolicyOptions>(options => { options.Secure = CookieSecurePolicy.Always; });
app.UseCookiePolicy();
Всё, с этой проблемой справились. Заходим, пробуем, и снова тоже самое. Изучаем, значиться, дальше. И наконец находим, что файлы cookie то установились, но по неверному маршруту. Значит теперь самим вручную нужно указать маршрут установки. Делается это следующим образом, в настройках аутентификации OpenIdConnect
options.CorrelationCookie.Path = "/app1/signin-oidc"; options.NonceCookie.Path = "/app1/signin-oidc";
И наконец все работает! Полный код файла program.cs ниже.
var builder = WebApplication.CreateBuilder(args); IConfiguration configuration = builder.Configuration; builder.Services.AddHttpClient(); builder.Services.AddOptions(); builder.Services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }).AddCookie() .AddOpenIdConnect(options => { options.SignInScheme = "Cookies"; options.Authority = "https://domain.com/server"; options.ClientId = "ClientId"; options.ClientSecret = "ClientSecret"; options.ResponseType = OpenIdConnectResponseType.Code; options.UsePkce= true; options.Scope.Add("openid"); options.Scope.Add("profile"); options.SaveTokens = true; options.GetClaimsFromUserInfoEndpoint = true; options.CorrelationCookie.Path = "/app1/signin-oidc"; options.NonceCookie.Path = "/app1/signin-oidc"; options.Events.OnRedirectToIdentityProvider = ctx => { ctx.ProtocolMessage.RedirectUri = "https://domain.com/server/signin-oidc"; return Task.CompletedTask; }; options.Events.OnRedirectToIdentityProviderForSignOut = ctx => { ctx.ProtocolMessage.PostLogoutRedirectUri = "https://domain.com/server/signout-callback-oidc"; return Task.CompletedTask; }; }); builder.Services.Configure<CookiePolicyOptions>(options => { options.Secure = CookieSecurePolicy.Always; }); var app = builder.Build(); IWebHostEnvironment env = app.Environment; if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseWebAssemblyDebugging(); } else { app.UseHsts(); } app.UsePathBase("/App1"); app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseCookiePolicy(); app.UseAuthentication(); app.UseAuthorization(); app.MapRazorPages(); app.MapControllers(); app.MapFallbackToFile("Index.html"); app.Run();
Небольшое послесловие
Возможно описанный мной способ решения данного кейса не на 100% верный, но разрабатывался он исходя из тех исходных данных, которые были нам предоставлены. В просторах интернета не нашел похожего решения данного кейса, или хотя бы похожего, поэтому решил поделиться. Если у вас есть, вариант как можно усовершенствовать этот способ или сделать по иному но, в тех же рамках, то прошу в комментарии.
