Предисловие

Сегодня в этой статье я хочу поделиться личным опытом работы и решением конкретного кейса. Работали над ним небольшой командой, но для простоты повествования буду писать от первого лица. Собственно сам кейс: есть 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. Путей решения этой проблемы два:

  1. Установить для наших приложений самозаверяющиеся SSL сертификаты. Как это сделать описано здесь.

  2. Либо на наших приложениях изменить конфигурацию политики файлов 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% верный, но разрабатывался он исходя из тех исходных данных, которые были нам предоставлены. В просторах интернета не нашел похожего решения данного кейса, или хотя бы похожего, поэтому решил поделиться. Если у вас есть, вариант как можно усовершенствовать этот способ или сделать по иному но, в тех же рамках, то прошу в комментарии.