Современные аутентификации на .NET: OpenID Connect, BFF, SPA

Как продолжают развиваться web технологии, также развиваются методы и протоколы, направленные на их безопасность. Протоколы OAuth 2.0 и OpenID Connect сильно эволюционировали в ответ на появляющиеся угрозы безопасности и усложняющуюся архитектуру web приложений. Традиционные методы аутентификации, которые были эффективны, сегодня становятся устаревшими для современных СПА (Single Page Applications), которые сталкиваются с новыми проблемами безопасности. В этом контексте появился архитектурный паттерн Backend-For-Frontend (BFF), рекомендованный как решение для организации взаимодействия между СПА и их backend системами, предлагая более безопасный и управляемый метод аутентификации и управления сеансами. В этой статье будет исследован внутреннее устройство BFF паттерна, демонстрируя его практическое применение через минимальное решение, реализованное с использованием .NET и React. By the end, вы будете иметь ясное понимание того, как использовать BFF паттерн, чтобы улучшить безопасность и функциональность ваших web приложений.

Исторический контекст

История OAuth 2.0 и OpenID Connect отражает непрерывное развитие Интернет технологий. Disegliate un’occhiata più da vicino ai questi protocolli e al loro impatto sugli applicativi web moderni.

Introdotta nel 2012, il protocollo OAuth 2.0 è diventato uno standard adottato ampiamente per l’autorizzazione.Permette alle applicazioni terze di ottenere accesso limitato ai risorse utente senza esporre le credenziali utente al client. OAuth 2.0 supporta diversi flussi, ognuno progettato per adattarsi flessibilmente a vari casi d’uso.

Создаваясь на основе OAuth 2.0, протокол OpenID Connect (OIDC) появился в 2014 году, добавив необходимые функции идентификации. Он обеспечивает клиентским приложениям стандартный метод для проверки идентификации пользователя и получения основной информации о нём через стандартизированный接入点 или путем приобретения ID-токена в формате JWT (JSON Web Token).

Эволюция модели угроз

С развитием функциональных возможностей и популярности SPA, модель угроз для SPA также эволюционировала. Vulnerabilities таких как Cross-Site Scripting (XSS) и Cross-Site Request Forgery (CSRF) стали более распространенными. since SPA часто взаимодействуют с сервером через API, securely storing и using access tokens и refresh tokens стало критически важно для безопасности.

В ответ на требования времени, протоколы OAuth и OpenID Connect продолжают эволюционировать, чтобы адаптироваться к новым вызовам, появляющимся с новыми технологиями и увеличивающейся количество угроз.meanwhile, постоянное развитие угроз и улучшение security practices mean, что устаревшие подходы больше не удовлетворяют современным требованиям безопасности. As a result, протокол OpenID Connect в настоящее время предлагает широкий спектр возможностей, но многие из них уже или вскоре будут считаться устаревшими и часто небезопасными. This diversity creates difficulties для разработчиков SPA в выборе наиболее соответствующего и безопасного способа для взаимодействия с сервером OAuth 2.0 и OpenID Connect.

В частности, Implicit Flow сейчас можно считать устаревшим, и для любого типа клиента, будь то SPA, мобильное приложение или desktop-приложение, теперь настоятельно рекомендуется использовать Flux Autorization Code вместе с Proof Key for Code Exchange (PKCE).

Безопасность современных SPA

Почему современные SPA до сих пор считаются уязвимыми, даже при использовании Flux Autorization Code с PKCE? Есть несколько ответов на этот вопрос.

Уязвимости в JavaScript-коде

JavaScript – это мощный язык программирования, играющий ключевую роль в современных Сингл-Паge Applications (SPA). Однако его широкие возможности и широкое распространение представляют собой потенциальную угрозу. Современные SPA, построенные на библиотеках и фреймворках, таких как React, Vue или Angular, используют огромное количество библиотек и зависимостей. Их можно увидеть в папке node_modules, и количество таких зависимостей может достигать сотен или даже тысяч. Каждая из этих библиотек может содержать уязвимости различной степени важности, и разработчики SPA не имеют возможности тщательно проверить код всех используемых зависимостей. Часто разработчики даже не следят за полной list of dependencies, так как они являются транзитными зависимостями друг от друга. Е даже если их собственный код будет разработан до высших стандартов качества и безопасности, нельзя быть полностью уверенным отсутствия уязвимостей в готовом приложении.

Злонамеренный код JavaScript, который может быть внедрен в приложение различными способами, через атаки, такие как межсайтовый скриптинг (XSS) или через компрометацию сторонних библиотек, получает те же привилегии и уровень доступа к данным, что и легитимный код приложения. Это позволяет злонамеренному коду красть данные с текущей страницы, взаимодействовать с интерфейсом приложения, отправлять запросы на бэкенд, красть данные из локального хранилища (localStorage, IndexedDB) и даже инициировать аутентификационные сеансы самостоятельно, получая свои собственные токены доступа, используя тот же поток Authorization Code и PKCE.

Уязвимость Spectre

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

Во-первых, SPA интенсивно используют JavaScript для управления состоянием приложения и взаимодействия с сервером. Это увеличивает поверхность атаки для злонамеренного JavaScript-кода, который может использовать уязвимости Spectre. Во-вторых, в отличие от традиционных многостраничных приложений (MPA), SPA редко перезагружаются, что означает, что страница и загруженный код остаются активными в течение длительного времени. Это дает злоумышленникам значительно больше времени для проведения атак с использованием злонамеренного JavaScript-кода.

Уязвимости Spectre позволяют злоумышленникам красть токены доступа, хранящиеся в памяти JavaScript-приложения, что позволяет получить доступ к защищенным ресурсам, выдавая себя за легитимное приложение. Спекулятивное выполнение также может быть использовано для кражи данных сеанса пользователя, что позволяет злоумышленникам продолжать атаки даже после закрытия SPA.

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

Что сделать?

Давайте обобщим важное полученное заключение. Современные СПА, зависящие от большого количества внешних JavaScript-библиотек и работающие в браузерной среде на пользовательских устройствах, операцируют в программной и аппаратной среде, которую разработчики не могут полностью контролировать. Таким образом, мы должны рассмотреть такие приложения как принципиально уязвимые.

В ответ на перечисленные угрозы, больше специалистов склонны к полному исключению хранения токенов в браузере и проектированию приложений так, чтобы доступные и обновляемые токены получались и обрабатывались только серверной частью приложения, и они никогда не передавались на сторону браузера. В контексте СПА с backend-ом это может быть достигнуто с использованием архитектурногоpattern “Backend-For-Frontend (BFF)”.

Схема взаимодействия между сервером авторизации (OP), клиентом (RP), реализующим BFF-паттерн, и третьей стороной API (ressource Server) выглядит следующим образом:

Использование BFF-паттерна для защиты СПА предлагает несколько преимуществ. Токены доступа и обновления хранятся на серверной стороне и никогда не передаются на сторону браузера, что предотвращает их кражу в результате уязвимостей. Управление сессией и токенами осуществляется на серверной стороне, что позволяет обеспечить лучшую контроль безопасности и более надежную проверку аутентификации. Клиентское приложение взаимодействует с сервером через BFF, который упрощает логику приложения и уменьшает риск выполнения вредоносного кода.

Реализация pattern Backend-For-Frontend на платформе .NET

Перед тем, как мы перейдем к практической реализации BFF на платформе .NET, давайте рассмотрим необходимые ее компоненты и планируем наши действия. предположим, у нас уже есть настроенный сервер OpenID Connect, и нам нужно разработать SPA, работающее с backend-ом, внедрить аутентификацию с использованием OpenID Connect и организовать взаимодействие между серверными и клиентскими частями с использованием модели BFF.

Согласно документу OAuth 2.0 for Browser-Based Applications, архитектурная модель BFF предполагает, что backend выступает в качестве клиента OpenID Connect, использует токен авторизации с жизненным циклом Code Flow с PKCE для аутентификации, получает и хранит токены доступа и обновления на своей стороне и никогда не передает их SPA side в браузере. Также модель BFF предполагает наличие API на backend side, состоящего из четырех основных endpoint:

  1. Check Session: служит для проверки активного сеанса аутентификации пользователя. Типически вызывается SPA с использованием асинхронного API (fetch) и, если успешно, возвращает информацию о пользователе, который сейчас зашел. Таким образом, SPA, загруженный с третьего источника (например, CDN), может проверить статус аутентификации и либо продолжить свою работу с пользователем либо перейти к аутентификации с использованием сервера OpenID Connect.
  2. Вход: запускает процесс аутентификации на сервере OpenID Connect. Типично, если SPA не может получить данные пользователя в авторизованном состоянии на шаге 1 через Check Session, он перенаправляет браузер на этот URL, который затем формирует полный запрос для сервера OpenID Connect и перенаправляет браузер туда.
  3. Войти: принимает код авторизации, отправленный сервером после шага 2 в случае успешной аутентификации. Осуществляет прямой запрос к серверу OpenID Connect для обмена кода авторизации + код проверки PKCE на токены доступа и обновления. Инициирует авторизованную сессию на клиентской стороне, выдавая пользователю аутентификационный кук.
  4. Выход: служит для завершения сессии аутентификации. Типично, SPA перенаправляет браузер на этот URL, который в свою очередь формирует запрос к точке конца сессии на сервере OpenID Connect для завершения сессии, а также удаляет сессию на клиентской стороне и аутентификационный кук.

Теперь рассмотрим инструменты, которые предоставляет платформа .NET в комплекте и посмотрим, что мы можем использовать для реализации модели BFF. Платформа .NET предлагает NuGet-пакет Microsoft.AspNetCore.Authentication.OpenIdConnect, который является готовой реализацией клиента OpenID Connect, поддерживаемого Microsoft. Этот пакет поддерживает идентификационный поток кода авторизации и PKCE, и он добавляет endpoint с относительным путем /signin-oidc, который уже реализует необходимую функциональность точки входа в сессию (см. пункт 3 выше). Таким образом, мы должны реализовать еще три оставшихся endpoint-а.

Для практического примера интеграции мы рассмотрим тестовый сервер OpenID Connect на базе библиотеки Abblix OIDC Server. Однако все, что указано ниже, применяется и для других серверов, включая открытые сервера Facebook, Google, Apple и других, соответствующих спецификации OpenID Connect.

Для реализации SPA на前端 нам потребуется использовать React, а на backend — .NET WebAPI. Это одна из наиболее распространенных технологических платформ на момент написания этой статьи.

Общая схема компонентов и их взаимодействия выглядит следующим образом:

Для работы с примерами из этой статьи вам также потребуется установить .NET SDK и Node.js. Все примеры в этой статье были разработаны и протестированы с использованием .NET 8, Node.js 22 и React 18, которые были актуальны на момент написания этой статьи.

Создание клиентского SPA на React с backend на .NET

Для быстрой создания клиентской приложения удобно использовать готовую шаблон. До версии .NET 7 SDK предоставлял встроенный шаблон для приложения .NET WebAPI и React SPA. К сожалению, этот шаблон был убран в версии .NET 8. Поэтому команда Abblix создала свой собственный шаблон, который включает backend .NET WebApi, frontend SPA на React и TypeScript, построенный с использованием Vite. Этот шаблон является общедоступным в составе пакета Abblix.Templates, и вы можете установить его, выполнив следующий приказ:

Shell

 

dotnet new install Abblix.Templates

Теперь мы можем использовать шаблон с именем abblix-react. Используем его для создания нового приложения под названием BffSample:

Shell

 

dotnet new abblix-react -n BffSample

Этот приказ создает приложение, состоящее из backend .NET WebApi и клиентского React SPA. Файлы, связанные с SPA, расположены в папке BffSample\ClientApp.

После создания проекта система вас просят запустить команду для установки зависимостей:

Shell

 

cmd /c "cd ClientApp && npm install"

Для успешного запуска проекта рекомендуется согласиться и выполнить эту команду, введя Y (да).

Давайте сразу изменим номер порта, на котором работает приложение BffSample локально, на 5003. Это действие не обязательно, но оно упростят дальнейшую настройку сервера OpenID Connect. Для этого откройте файл BffSample\Properties\launchSettings.json, найдите профиль с именем https и измените значение свойства applicationUrl на https://localhost:5003.

Далее добавьте NuGet-пакет, реализующий клиент OpenID Connect, перейдя в папку BffSample и выполняя следующий комmand:

Shell

 

dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

Установите два схемы аутентификации с именем Cookies и OpenIdConnect в приложении, читая их настройки из конфигурации приложения. Для этого измените файл BffSample\Program.cs:

C#

 

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// ******************* START *******************
var configuration = builder.Configuration;

builder.Services
    .AddAuthorization()
    .AddAuthentication(options => configuration.Bind("Authentication", options))
    .AddCookie()
    .AddOpenIdConnect(options => configuration.Bind("OpenIdConnect", options));
// ******************** END ********************
var app = builder.Build();

И добавьте необходимые настройки для подключения к серверу OpenID Connect в файле BffSample\appsettings.json:

JSON

 

{
  // ******************* START *******************
  "Authentication": {
      "DefaultScheme": "Cookies",
      "DefaultChallengeScheme": "OpenIdConnect"
  },
  "OpenIdConnect": {
      "SignInScheme": "Cookies",
      "SignOutScheme": "Cookies",
      "SaveTokens": true,
      "Scope": ["openid", "profile", "email"],
      "MapInboundClaims": false,
      "ResponseType": "code",
      "ResponseMode": "query",
      "UsePkce": true,
      "GetClaimsFromUserInfoEndpoint": true
  },
  // ******************** END ********************
  "Logging": {
    "LogLevel": {
      "Default": "Information",

А также в файле BffSample\appsettings.Development.json:

JSON

 

{
  // ******************* START *******************
  "OpenIdConnect": {
      "Authority": "https://localhost:5001",
      "ClientId": "bff_sample",
      "ClientSecret": "secret"
  },
  // ******************** END ********************
  "Logging": {
    "LogLevel": {
      "Default": "Information",

Проведите быстрый обзор каждой настройки и ее цели:

  • Раздел аутентификации: свойство DefaultScheme устанавливает аутентификацию по умолчанию с использованием схемы Cookies, а DefaultChallengeScheme делегирует выполнение аутентификации схеме OpenIdConnect, если пользователь не может быть аутентифицирован по умолчанию. Таким образом, когда пользователь неизвестно для приложения, будет вызван сервер OpenID Connect для аутентификации, и после этого авторизованному пользователю будет выдан кукет аутентификации. Все последующие вызовы сервера будут идентифицированы этим кукетом, без необходимости контактировать сервер OpenID Connect.
  • секция OpenIdConnect:
    • SignInScheme и SignOutScheme свойства указаны для Cookies схемы, которая будет использоваться для сохранения информации пользователя после входа.
    • Свойство Authority содержит базовый URL сервера OpenID Connect. ClientId и ClientSecret указывают идентификатор и секретный ключ клиентской приложения, зарегистрированного на сервере OpenID Connect.
    • SaveTokens указывает на необходимость сохранять токены, полученные в результате аутентификации с сервера OpenID Connect.
    • Scope содержит список областей, к которым приложение BffClient требует доступа. В данном случае требуются стандартные области openid (идентификатор пользователя), profile (профиль пользователя) и email (почта).
    • MapInboundClaims отвечает за преобразование исходящих требований от сервера OpenID Connect в требования, используемые в приложении. Значение false意味着 требования будут сохранены в сессии аутентифицированного пользователя в том виде, в котором они получены от сервера OpenID Connect.
    • ResponseType с значением code указывает, что клиент будет использовать Flux авторизации.
    • ResponseMode указывает на передачу кода авторизации в URL-запросе, что является стандартным методом для Flux авторизации.
    • Свойство UsePkce указывает на необходимость использования PKCE в процессе аутентификации для предотвращения подмены кода авторизации.
    • Свойство GetClaimsFromUserInfoEndpoint указывает, что данные профиля пользователя следует получать от UserInfo endpoint.

Поскольку наша приложение предполагает, что взаимодействие с пользователем происходит только после аутентификации, мы должны убедиться, что React SPA загружается только после успешной аутентификации. Конечно, если SPA загружается из внешнего источника, например, от Static Web Host, например, от серверов Content Delivery Network (CDN) или локального сервера, запущенного с помощью команды npm start (например, во время запуска нашего примера в отладочном режиме), невозможно проверить статус аутентификации до загрузки SPA. Но, когда наш собственный .NET backend отвечает за загрузку SPA, это возможно.

Для этого добавьте middleware, ответственный за аутентификацию и авторизацию, в файле BffSample\Program.cs:

C#

 

app.UseRouting();
// ******************* START *******************
app.UseAuthentication();
app.UseAuthorization();
// ******************** END ********************

В конце файла BffSample\Program.cs, где происходит переход к загрузке SPA, добавьте требование авторизации, .RequireAuthorization():

C#

 

app.MapFallbackToFile("index.html").RequireAuthorization();

Установка сервера OpenID Connect.

Как упомянуто ранее, для примера практической интеграции мы будем использовать тестовый сервер OpenID Connect, основанный на библиотеке Abblix OIDC Server. Базовая модель приложения на основе ASP.NET Core MVC с библиотекой Abblix OIDC Server также доступна в пакете Abblix.Templates, который мы установили ранее. Используем этот шаблон для создания нового приложения с именем OpenIDProviderApp:

Shell

 

dotnet new abblix-oidc-server -n OpenIDProviderApp

Для настройки сервера нам нужно зарегистрировать приложение BffClient в качестве клиента на сервере OpenID Connect и добавить тестового пользователя. Для этого добавьте следующие блоки в файл OpenIDProviderApp\Program.cs:

C#

 

var userInfoStorage = new TestUserStorage(
    // ******************* START *******************
    new UserInfo(
        Subject: "1234567890",
        Name: "John Doe",
        Email: "[email protected]",
        Password: "Jd!2024$3cur3")
    // ******************** END ********************
);
builder.Services.AddSingleton(userInfoStorage);

// ...

// Register and configure Abblix OIDC Server
builder.Services.AddOidcServices(options =>
{
    // Configure OIDC Server options here:
    // ******************* START *******************
    options.Clients = new[] {
        new ClientInfo("bff_sample") {
            ClientSecrets = new[] {
                new ClientSecret {
                    Sha512Hash = SHA512.HashData(Encoding.ASCII.GetBytes("secret")),
                }
            },
            TokenEndpointAuthMethod = ClientAuthenticationMethods.ClientSecretPost,
            AllowedGrantTypes = new[] { GrantTypes.AuthorizationCode },
            ClientType = ClientType.Confidential,
            OfflineAccessAllowed = true,
            PkceRequired = true,
            RedirectUris = new[] { new Uri("https://localhost:5003/signin-oidc", UriKind.Absolute) },
            PostLogoutRedirectUris = new[] { new Uri("https://localhost:5003/signout-callback-oidc", UriKind.Absolute) },
        }
    };
    // ******************** END ********************
    // The following URL leads to Login action of the AuthController
    options.LoginUri = new Uri($"/Auth/Login", UriKind.Relative);

    // The following line generates a new key for token signing. Replace it if you want to use your own keys.
    options.SigningKeys = new[] { JsonWebKeyFactory.CreateRsa(JsonWebKeyUseNames.Sig) };
});

Давайте детально просмотрим этот код. Мы регистрируем клиента с идентификатором `bff_sample` и секретным ключом `secret` (hranя его в виде SHA512 хэша), указывая, что для получения токена будет использоваться аутентификация клиента с секретным ключом, отправленным в POST сообщение (`ClientAuthenticationMethods.ClientSecretPost`). Переменная `AllowedGrantTypes` указывает, что клиент может использовать только Flуд авторизации с кодом авторизации. `ClientType` определяет клиента как конфиденциального, что意味着 он может безопасно хранить свой секретный ключ. `OfflineAccessAllowed` позволяет клиенту использовать обновляемые токены. `PkceRequired` обязывает использовать PKCE в процессе аутентификации. `RedirectUris` и `PostLogoutRedirectUris` содержат списки допустимых URL-адресов для перенаправления после аутентификации и завершения сессии, соответственно.

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

Реализация базового API BFF

ранее мы упомянули, что использование пакета `Microsoft.AspNetCore.Authentication.OpenIdConnect` автоматически добавляет реализацию точки входа Sign In в наше примерное приложение. Теперь пришло время реализовать остальную часть API BFF. Мы будем использовать ASP.NET MVC контроллер для этих дополнительных endpoint-ов. Начнем с добавления папки `Controllers` и файла `BffController.cs` в проект `BffSample` следующим кодом внутри:

C#

 

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;

namespace BffSample.Controllers;

[ApiController]
[Route("[controller]")]
public class BffController : Controller
{
    public const string CorsPolicyName = "Bff";

    [HttpGet("check_session")]
    [EnableCors(CorsPolicyName)]
    public ActionResult> CheckSession()
    {
        // возвращаем 401 Unauthorized, чтобы SPA был перенаправлен на точку входа в систему авторизации
        if (User.Identity?.IsAuthenticated != true)
            return Unauthorized();

        return User.Claims.ToDictionary(claim => claim.Type, claim => claim.Value);
    }

    [HttpGet("login")]
    public ActionResult> Login()
    {
        // логика инициирования процесса авторизации кодом
        return Challenge(new AuthenticationProperties { RedirectUri = Url.Content("~/") });
    }

    [HttpPost("logout")]
    public IActionResult Logout()
    {
        // логика управления выходом пользователя
        return SignOut();
    }
}

Давайте разbreak down этого классного кода:

  • Атрибут [Route("[controller]")] устанавливает базовый маршрут для всех действий контроллера. В этом случае, маршрут соответствует имени контроллера,意味着 все пути к нашим API-методам будут начинаться с /bff/.
  • Константа CorsPolicyName = "Bff" определяет имя политики CORS (Cross-Origin Resource Sharing), которая будет использоваться в атрибутах методов. позднее на нее ссылаться.
  • Три метода CheckSession, Login и Logout реализуют необходимую функциональность BFF, описанную выше. Они обрабатывают GET-запросы на /bff/check_session, /bff/login и POST-запросы на /bff/logout соответственно.
  • Метод CheckSession проверяет статус авторизации пользователя. Если пользователь не авторизован, он возвращает код 401 Unauthorized, который должен force SPA перенаправлять на точку входа в систему авторизации. Если авторизация пройдена успешно, метод возвращает набор заявлений и их значений. Этот метод также включает привязку политики CORS с именем CorsPolicyName, поскольку вызов этого метода может быть cross-domain и содержать cookies, используемые для аутентификации пользователя.
  • Метод Login вызывается SPA, если предыдущий вызов CheckSession вернул 401 Unauthorized. Он убеждает, что пользователь по-прежнему не аутентифицирован и инициирует настроенный Challenge процесс, который приведет к перенаправлению на сервер OpenID Connect, аутентификации пользователя с использованием Flux Autorization Code и PKCE, и выдачей куки аутентификации. после этого контроль возвращается к корню нашего приложения "~/", который вызовет SPA для перезагрузки и запуска с аутентифицированным пользователем.
  • Метод Logout также вызывается SPA, но завершает текущую сессию аутентификации. Он удаляет куки аутентификации, выданные серверной частью BffSample, и также вызывает End Session endpoint на стороне сервера OpenID Connect.

Конфигурация CORS для BFF

Как упомянуто выше, метод CheckSession предназначен для асинхронных вызовов от SPA (обычно с использованием Fetch API). правильная работа этого метода зависит от возможности отправки куки аутентификации из браузера. Если SPA загружается с различного Static Web Host, такого как CDN или dev сервер, работающий на отдельном порту, этот вызов становится cross-domain. Это требует настроения стратегии CORS, иначе SPA не сможет вызывать этот метод.

Мы уже указали в коду контроллера в файле Controllers\BffController.cs, что для использования должна быть стратегия CORS с именем CorsPolicyName = "Bff". пришло время настроить параметры этой стратегии, чтобы решить наше задание. вернемся к файлу BffSample/Program.cs и добавим следующие блоки кода:

C#

 


// ******************* START *******************
using BffSample.Controllers;
// ******************** END ********************
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();

// ...

builder.Services
    .AddAuthorization()
    .AddAuthentication(options => configuration.Bind("Authentication", options))
    .AddCookie()
    .AddOpenIdConnect(options => configuration.Bind("OpenIdConnect", options));
// ******************* START *******************
builder.Services.AddCors(
    options => options.AddPolicy(
        BffController.CorsPolicyName,
        policyBuilder =>
        {
            var allowedOrigins = configuration.GetSection("CorsSettings:AllowedOrigins").Get();

            if (allowedOrigins is { Length: > 0 })
                policyBuilder.WithOrigins(allowedOrigins);

            policyBuilder
                .WithMethods(HttpMethods.Get)
                .AllowCredentials();
        }));
// ******************** END ********************
var app = builder.Build();

Этот код позволяет вызывать политику CORS с SPAs, загружаемых из источников, указанных в конфигурации в виде массива строк CorsSettings:AllowedOrigins, используя метод GET и позволяет отправлять куки в этом вызове. Кроме того, убедитесь, что вызов app.UseCors(...) располагается just before app.UseAuthentication():

C#

 

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// ******************* START *******************
app.UseCors(BffController.CorsPolicyName);
// ******************** END ********************
app.UseAuthentication();
app.UseAuthorization();

Для correct работы политики CORS добавьте соответствующий настройки в файл конфигурации BffSample\appsettings.Development.json:

JSON

 

{
  // ******************* START *******************
  "CorsSettings": {
    "AllowedOrigins": [ "https://localhost:3000" ]
  },
  // ******************** END ********************
 "OpenIdConnect": {
   "Authority": "https://localhost:5001",
   "ClientId": "bff_sample",

В нашем примере адрес https://localhost:3000 это адрес, откуда запущен dev сервер с React SPA, используя комманду npm run dev. Этот адрес можно найти в вашем случае, открыв файл BffSample.csproj и найдя значение параметра SpaProxyServerUrl. В реальном приложении политика CORS может включать адрес вашего CDN (Content Delivery Network) или схожее обслуживание. важно помнить, что если SPA загружается с другого адреса и порта, чем тот, который обеспечивает BFF API, вам следует добавить этот адрес в конфигурацию CORS.

Реализация аутентификации via BFF в React-приложении

Мы реализовали BFF API на серверной стороне. Сейчас нам пора сосредоточиться на React SPA и добавить соответствующую функциональность для вызова этого API. Начнем с перехода в папку BffSample\ClientApp\src\, создания папки components и добавления файла Bff.tsx следующим содержимым:

TypeScript

 

import React, { createContext, useContext, useEffect, useState, ReactNode, FC } from 'react';

// Определение формы контекста BFF  

interface BffContextProps {
    user: any;
    fetchBff: (endpoint: string, options?: RequestInit) => Promise;
    checkSession: () => Promise;
    login: () => void;
    logout: () => Promise;
}

  
// Создание контекста для BFF, чтобы разделить состояние и функции по всему приложению  

const BffContext = createContext({
    user: null,
    fetchBff: async () => new Response(),
    checkSession: async () => {},
    login: () => {},
    logout: async () => {}
});

interface BffProviderProps {
    baseUrl: string;
    children: ReactNode;
}

export const BffProvider: FC = ({ baseUrl, children }) => {
    const [user, setUser] = useState(null);

      
// Нормализация базового URL путем удаления завершающего слэша для предотвращения несогласованности URL  

    if (baseUrl.endsWith('/')) {
        baseUrl = baseUrl.slice(0, -1);
    }

    const fetchBff = async (endpoint: string, options: RequestInit = {}): Promise => {
        try {
              
// Функция fetch включает учетные данные для обработки cookie, необходимые для аутентификации  

            return await fetch(`${baseUrl}/${endpoint}`, {
                credentials: 'include',
                ...options
            });
        } catch (error) {
            console.error(`Error during ${endpoint} call:`, error);
            throw error;
        }
    };

      
// Функция login перенаправляет на страницу входа, когда пользователю необходимо пройти аутентификацию  

    const login = (): void => {
        window.location.replace(`${baseUrl}/login`);
    };

      
// Функция checkSession отвечает за проверку пользовательской сессии при первом рендере  

    const checkSession = async (): Promise => {
        const response = await fetchBff('check_session');

        if (response.ok) {
              
// Если сессия действительна, обновите состояние пользователя с полученными данными о правах  

            setUser(await response.json());
        } else if (response.status === 401) {
              
// Если пользователь не аутентифицирован, перенаправьте его на страницу входа  

            login();
        } else {
            console.error('Unexpected response from checking session:', response);
        }
    };

      
// Функция выхода пользователя из системы  

    const logout = async (): Promise => {
        const response = await fetchBff('logout', { method: 'POST' });

        if (response.ok) {
              
// Перенаправление на главную страницу после успешного выхода  

            window.location.replace('/');
        } else {
            console.error('Logout failed:', response);
        }
    };

      
// useEffect используется для выполнения функции checkSession сразу после монтирования компонента  

      
// Это гарантирует проверку сессии сразу при загрузке приложения  

    useEffect(() => { checkSession(); }, []);

    return (
          
// Предоставление контекста BFF с релевантными значениями и функциями для использования по всему приложению  

        
            {children}
        
    );
};

  
// Пользовательский хук для легкого использования контекста BFF в других компонентах  

export const useBff = (): BffContextProps => useContext(BffContext);

  
// Экспорт HOC для предоставления доступа к контексту BFF  

export const withBff = (Component: React.ComponentType) => (props: any) =>
    
        {context => }
    ;

Этот файл экспортирует:

  • Компонент BffProvider, который создает контекст для BFF и предоставляет функции и состояние, связанные с аутентификацией и управлением сессиями для всего приложения.
  • Пользовательский хук useBff(), который возвращает объект с текущим состоянием пользователя и функциями для работы с BFF: checkSession, login и logout. Он предназначен для использования в функциональных компонентах React.
  • Компонент высшего порядка (HOC) withBff для использования в классовых компонентах React.

Далее создайте компонент UserClaims, который будет отображать претензии текущего пользователя после успешной аутентификации. Создайте файл UserClaims.tsx в папке BffSample\ClientApp\src\components с следующим содержимым:

TypeScript

 

import React from 'react';
import { useBff } from './Bff';

export const UserClaims: React.FC = () => {
    const { user } = useBff();

    if (!user)
        return <div>Checking user session...</div>;

    return (
        <>
            <h2>User Claims</h2>
            {Object.entries(user).map(([claim, value]) => (
                <div key={claim}>
                    <strong>{claim}</strong>: {String(value)}
                </div>
            ))}
        </>
    );
};

Этот код проверяет наличие аутентифицированного пользователя с использованием хука useBff() и отображает претензии пользователя в виде списка, если пользователь аутентифицирован. Если данные пользователя еще не доступны, отображается текст Проверка сеанса пользователя....

Теперь перейдите к файлу BffSample\ClientApp\src\App.tsx. Замените его содержимое на необходимый код. Импортируйте BffProvider из components/Bff.tsx и UserClaims из components/UserClaims.tsx и вставьте основной код компонента:

TypeScript

 

import { BffProvider, useBff } from './components/Bff';
import { UserClaims } from './components/UserClaims';

const LogoutButton: React.FC = () => {
    const { logout } = useBff();
    return (
        <button className="logout-button" onClick={logout}>
            Logout
        </button>
    );
};

const App: React.FC = () => (
    <BffProvider baseUrl="https://localhost:5003/bff">
        <div className="card">
            <UserClaims/>
        </div>
        <div className="card">
            <LogoutButton />
        </div>
    </BffProvider>
);

export default App;

Здесь параметр baseUrl указывает базовый URL нашего BFF API https://localhost:5003/bff. Это упрощение сделано намеренно для простоты. В реальном приложении вы должны предоставлять эту настройку динамически, а не жестко кодировать ее. Существует множество способов достичь этого, но обсуждение их выходит за рамки данной статьи.

Кнопка Выход позволяет пользователю выйти из учетной записи. Она вызывает функцию logout, доступную через хук useBff, и перенаправляет браузер пользователя на endpoint /bff/logout, который завершает сессию пользователя с серверной стороны.

На этой стадии вы можете запустить приложение BffSample вместе с OpenIDProviderApp и протестировать его функциональность. Вы можете использовать команду dotnet run -lp https в каждом проекте или вашем любимом IDE, чтобы запустить их. оба приложения должны быть запущены одновременно.

После этого откройте свой браузер и перейдите к адресу https://localhost:5003. Если все настроено правильно, SPA загрузится и вызовет /bff/check_session. Endpoint /check_session вернет ответ 401, заставляя SPA перенаправить браузер на /bff/login, который затем инициирует аутентификацию на сервере с использованием OpenID Connect Authorization Code Flow с использованием PKCE. Эту последовательность запросов вы можете наблюдать, открыв консоль разработки в вашем браузере и перейдя к вкладке Network. После успешного ввода учетных данных пользователя ([email protected], Jd!2024$3cur3), управление возвращается в SPA, и вы увидите заявки авторизованного пользователя в браузере:

Plain Text

 

sub: 1234567890
sid: V14fb1VQbAFG6JXTYQp3D3Vpa8klMLcK34RpfOvRyxQ
auth_time: 1717852776
name: John Doe
email: [email protected]

Кроме того, нажатие кнопки Выход перенаправляет браузер на /bff/logout, который закрывает сессию пользователя, и вы увидите страницу входа снова с просьбой ввести свой логин и пароль.

Если у вас возникли какие-либо ошибки, вы можете сравнить свой код с нашей репозиторий GitHub Abblix/Oidc.Server.GettingStarted, который содержит этот и другие примеры, готовые для запуска.

Решение проблем доверия к SSL-сертификатам HTTPS

При локальном тестировании веб-приложений, настроенных для работы через HTTPS, у вас могут возникнуть ошибки браузера о том, что SSL-сертификат не доверен. Эта проблема возникает из-за того, что сертификаты разработки, используемые ASP.NET Core, не выданы признанной Certificate Authority (CA), а являются самоподписанными или вообще отсутствуют в системе. Эти ошибки могут быть устранены путем выполнения следующего команды один раз:

Shell

 

dotnet dev-certs https --trust

Эта команда генерирует самоподписанный сертификат для localhost и устанавливает его в вашей системе, чтобы она доверяла этому сертификату. Сертификат будет использоваться ASP.NET Core для работы веб-приложений локально. После выполнения этой команды перезапустите ваш браузер, чтобы изменения вступили в силу.

Особая информация для пользователей Chrome: Если даже после установки сертификата разработки как доверенного некоторые версии Chrome еще могут ограничивать доступ к сайтам localhost по соображениям безопасности. Если у вас встретилась ошибка, указывающая, что ваша связь небезопасна и доступ к сайтам localhost заблокирован Chrome, вы можете обойти этот блокер следующим образом:

  • Щёлкните в любом месте на странице с ошибкой и напечатайте thisisunsafe или badidea, в зависимости от версии Chrome. Эти последовательности клавиш являются командами пропуска в Chrome и позволяют вам продолжить работу с сайтом localhost.

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

Вызов третьей-party API через BFF

Мы успешно внедрили аутентификацию в нашем приложении BffSample. Теперь let’s перейти к вызова третьей-party API, который требует токена доступа.

предположим, у нас есть отдельный сервис, который поставляет необходимые данные, например, прогнозы погоды, и доступ к нему разрешен только с токеном доступа. Rolя серверной части BffSample будет выполнять роль реverse proxy, то есть принимать и идентифицировать запрос данных от SPA, добавлять токен доступа к нему, пересылать этот запрос в сервис погоды и затем возвращать ответ сервиса обратно в SPA.

Создание сервиса ApiSample

Перед тем, как продемонстрировать вызов удаленного API через BFF, нам нужно создать приложение, которое будет выполнять эту роль API в нашем примере.

Для создания приложения мы будем использовать шаблон, предоставляемый .NET. Перейдите в папку, содержащую проекты OpenIDProviderApp и BffSample, и выполните следующийCOMMAND, чтобы создать приложение ApiSample:

Shell

 

dotnet new webapi -n ApiSample

Это приложение ASP.NET Core Minimal API выполняет single endpoint с путем /weatherforecast, который предоставляет данные о погоде в JSON формате.

Первым делом, измените случайно присвоенный порт, используемый приложением ApiSample локально, на постоянный порт 5004. Как было указано ранее, этот шаг не обязателен, но он упрощает нашу установку. Для этого, откройте файл ApiSample\Properties\launchSettings.json, найдите профиль с именем https и измените значение свойства applicationUrl на https://localhost:5004.

Теперь добавим NuGet-пакет для аутентификации JWT Bearer токена:

Shell

 

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Настройте схему аутентификации и политику авторизации с именем WeatherApi в файле ApiSample\Program.cs:

C#

 

// ******************* START *******************
using System.Security.Claims;
// ******************** END ********************
var builder = WebApplication.CreateBuilder(args);

// Добавление служб в контейнер.
// Узнать больше о настройке Swagger/OpenAPI можно на https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// ******************* START *******************
var configuration = builder.Configuration;

builder.Services
    .AddAuthentication()
    .AddJwtBearer(options => configuration.Bind("JwtBearerAuthentication", options));

const string policyName = "WeatherApi";

builder.Services.AddAuthorization(
    options => options.AddPolicy(policyName, policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireAssertion(context =>
        {
            var scopeValue = context.User.FindFirstValue("scope");
            if (string.IsNullOrEmpty(scopeValue))
                return false;

            var scope = scopeValue.Split(' ', StringSplitOptions.RemoveEmptyEntries);
            return scope.Contains("weather", StringComparer.Ordinal);
        });
    }));
// ******************** END ********************
var app = builder.Build();

Этот блок кода устанавливает аутентификацию, читая конфигурацию из настроек приложения, включая авторизацию с использованием JWT (JSON Web Tokens) и настраивает политику авторизации с именем WeatherApi. Политика авторизации WeatherApi устанавливает следующие требования:

  • policy.RequireAuthenticatedUser(): Убеждает, чтолько только авторизованные пользователи могут доступно защищенные ресурсы.
  • policy.RequireAssertion(context => ...): пользователь должен иметь заявку scope, которая включает значение weather. Так как заявка scope может содержать несколько значений, разделённых пробелами согласно RFC 8693, реальное значение scope разделяется на отдельные части, и полученный массив проверяется наличием требуемого значения weather.

Вместе эти условия гарантируют, что только аутентифицированные пользователи с ключом доступа, авторизированным для сферы weather, могут вызывать защищённый этой политикой энDPоINT.

Необходимо применить эту политику к энDPоINTу /weatherforecast. Добавьте вызов RequireAuthorization(), как показано ниже:

C#

 

app.MapGet("/weatherforecast", () =>
{

// ...

})
.WithName("GetWeatherForecast")
// ******************* START *******************
.WithOpenApi()
.RequireAuthorization(policyName);
// ******************** END ********************

Добавьте необходимые настройки конфигурации для схемы аутентификации в файл appsettings.Development.json приложения ApiSample:

JSON

 

{
  // ******************* START *******************
  "JwtBearerAuthentication": {
    "Authority": "https://localhost:5001",
    "MapInboundClaims": false,
    "TokenValidationParameters": {
      "ValidTypes": [ "at+jwt" ],
      "ValidAudience": "https://localhost:5004",
      "ValidIssuer": "https://localhost:5001"
    }
  },
  // ******************** END ********************
  "Logging": {
    "LogLevel": {
      "Default": "Information",

Посмотрим на каждую настройку в detail:

  • Authority: Это URL, указывающее на сервер авторизации OpenID Connect, который выдает JWT-токены. Аутентификационный провайдер, настроенный в приложении ApiSample, будет использовать этот URL, чтобы получить информацию, необходимую для проверки токенов, такую как ключи подписания.
  • MapInboundClaims: Эта настройка управляет том, как исходящие требования из JWT-токена сопоставляются с внутренними требованиями в ASP.NET Core. Она установлена в false, что意味着 требования будут использовать свои исходные названия из JWT.
  • TokenValidationParameters:
    • ValidTypes: Установлено в at+jwt, что согласно RFC 9068 2.1 указывает на токен доступа в формате JWT.
    • ValidAudience: Указывает, что приложение будет принимать токены, выданные для клиента https://localhost:5004.
    • ValidIssuer: Указывает, что приложение будет принимать токены, выданные сервером https://localhost:5001.

Дополнительная настройка OpenIDProviderApp

Сочетание сервиса аутентификации OpenIDProviderApp и клиентского приложения BffSample хорошо подходит для обеспечения аутентификации пользователей. Однако, для того чтобы включить вызовы удаленного API, нам нужно зарегистрировать приложение ApiSample как ресурс в OpenIDProviderApp. В нашем примере мы используем Abblix OIDC Server, который поддерживает RFC 8707: Resource Indicators for OAuth 2.0. Поэтому мы зарегистрируем приложение ApiSample как ресурс с областью weather. Если вы используете другой сервер OpenID Connect, который не поддерживает Resource Indicators, все равно рекомендуется зарегистрировать уникальную область для этого удаленного API (например, weather в нашем примере).

Добавьте следующий код в файл OpenIDProviderApp\Program.cs:

C#

 

// Регистрация и настройка Abblix OIDC Server
builder.Services.AddOidcServices(options => {
    // ******************* НАЧАЛО *******************
    options.Resources =
    [
        new(new Uri("https://localhost:5004", UriKind.Absolute), new ScopeDefinition("weather")),
    ];
    // ******************** КОНЕЦ ********************
    options.Clients = new[] {
        new ClientInfo("bff_sample") {

В этом примере мы регистрируем приложение ApiSample, указав его базовый адрес https://localhost:5004 в качестве ресурса и определив специфический скоп названный weather. В реальных приложениях, особенно тех, которые имеют сложные API, состоящие из множества endpoint’ов, рекомендуется определять отдельные скопы для каждого отдельного endpoint’а или группы связанных endpoint’ов. Этот подход позволяет осуществить более точное управление доступом и обеспечивает гибкость в управлении правами на доступ. Например, можно создавать различные скопы для различных операций, модулей приложения или уровней доступа пользователей, что позволяет осуществить более мелкозернистый контроль над теми частями API, которые могут быть доступны определенным пользователям.

Разъяснение BffSample для проксирования запросов к удаленному API

Клиентское приложение BffSample теперь должно делать больше, чем просто запрашивать токен доступа для ApiSample. Оно также должно обрабатывать запросы SPA к удаленному API. Это заключается в добавлении полученного токена доступа от службы OpenIDProviderApp к этим запросам, пересылании их на удаленный сервер и возвращении ответов сервера обратно в SPA. Essentially, BffSample needs to function as a reverse proxy server.

Вместо того, чтобы вручную реализовать проксирование запросов в нашем клиентском приложении, мы будем использовать YARP (Yet Another Reverse Proxy), готовое продукты, разработанное Microsoft. YARP – это реverse proxy server, написанный на .NET и доступен в качестве NuGet пакета.

Чтобы использовать YARP в приложении BffSample, сначала добавьте NuGet пакет:

Shell

 

dotnet add package Yarp.ReverseProxy

Потом добавьте следующие импорты в начале файла BffSample\Program.cs:

C#

 

using Microsoft.AspNetCore.Authentication;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using System.Net.Http.Headers;
using Yarp.ReverseProxy.Transforms;

Перед вызовом var app = builder.Build(); добавьте код:

C#

 

builder.Services.AddHttpForwarder();

И между вызовами app.MapControllerRoute() и app.MapFallbackToFile():

C#

 

app.MapForwarder(
    "/bff/{**catch-all}",
    configuration.GetValue("OpenIdConnect:Resource") ?? throw new InvalidOperationException("Unable to get OpenIdConnect:Resource from current configuration"),
    builderContext =>
    {
        // Убираем префикс "/bff" из пути запроса
        builderContext.AddPathRemovePrefix("/bff");

        builderContext.AddRequestTransform(async transformContext =>
        {
            // Получаем токен доступа, полученный ранее во время процесса аутентификации
            var accessToken = await transformContext.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
            
            // Добавляем заголовок с токеном доступа в запрос прокси
            transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        });
    }).RequireAuthorization();

Давайте посмотрим, что делает этот код:

  • builder.Services.AddHttpForwarder() регистрирует необходимые YARP сервисы в контейнере DI.
  • app.MapForwarder настраивает перенаправление запросов на другой сервер или концепт.
  • "/bff/{**catch-all}" – это шаблон пути, который противоречит реverse proxy. Все запросы, начинающиеся с /bff/, будут обрабатываться YARP. {**catch-all} используется для捕获 всех оставшихся частей URL после /bff/.
  • configuration.GetValue<string>("OpenIdConnect:Resource") использует конфигурацию приложения, чтобы получить значение из раздела OpenIdConnect:Resource. Это значение указывает адрес ресурса, к которому будут перенаправлены запросы. В нашем примере это значение будет https://localhost:5004 – базовая URL, где работает приложение ApiSample.
  • builderContext => ... добавляет необходимые преобразования, которые YARP выполнит для каждого входящего запроса от SPA. В нашем случае будет два таких преобразования:
    • builderContext.AddPathRemovePrefix("/bff") удаляет префикс /bff из исходного пути запроса.
    • builderContext.AddRequestTransform(async transformContext => ...) добавляет HTTP-заголовок Authorization к запросу, содержащий токен доступа, который был ранее получен во время аутентификации. Таким образом, запросы от SPA к удаленному API будут аутентифицированы с использованием токена доступа, даже несмотря на то, что сам SPA не имеет доступа к этому токену.
  • .RequireAuthorization() указывает, что для всех перенаправляемых запросов требуется авторизация. Только авторизованные пользователи смогут получить доступ к маршруту /bff/{**catch-all}.

Для запроса токена доступа для ресурса https://localhost:5004 во время аутентификации, добавьте параметр Resource с значением https://localhost:5004 в конфигурацию OpenIdConnect в файле BffSample/appsettings.Development.json:

JSON

 

  "OpenIdConnect": {
    // ******************* START *******************
    "Resource": "https://localhost:5004",
    // ******************** END ********************
    "Authority": "https://localhost:5001",
    "ClientId": "bff_sample",

Также добавьте другое значение weather в массив scope в файле BffSample/appsettings.json:

JSON

 

{
  "OpenIdConnect": {

    // ...

    // ******************* START *******************
    "Scope": ["openid", "profile", "email", "weather"],
    // ******************** END ********************

    // ...

  }
}

Примечания: В реальном проекте необходимо наблюдать за истечением срока действия токена. Когда токен находится в предполагаемом истечении срока действия, вы должны либо запросить новый токен заранее с помощью рефреш-токена от службы аутентификации, либо обработать ошибку запрета доступа от удаленной API, получив новый токен и повторно выполняя исходный запрос. Для краткости мы опустили эту часть в этой статье.

Запрос к API погоды через BFF в SPA-приложении

Backend теперь готов. У нас есть приложение ApiSample, которое реализует API с авторизацией на основе токенов, и приложение BffSample, которое включает в себя встроенный реverse proxy сервер для обеспечения безопасного доступа к этому API. Final шаг – добавить функциональность для запроса этого API и отображения полученных данных внутри React SPA.

Добавьте файл WeatherForecast.tsx в BffSample\ClientApp\src\components следующим содержимым:

TypeScript

 

import React, { useEffect, useState } from 'react';
import { useBff } from './Bff';

interface Forecast {
    date: string;
    temperatureC: number;
    temperatureF: number;
    summary: string;
}

interface State {
    forecasts: Forecast[];
    loading: boolean;
}

export const WeatherForecast: React.FC = () => {
    const { fetchBff } = useBff();
    const [state, setState] = useState<State>({ forecasts: [], loading: true });
    const { forecasts, loading } = state;

    useEffect(() => {
        fetchBff('weatherforecast')
            .then(response => response.json())
            .then(data => setState({ forecasts: data, loading: false }));
    }, [fetchBff]);

    const contents = loading
        ? <p><em>Loading...</em></p>
        : (
            <table className="table table-striped" aria-labelledby="tableLabel">
                <thead>
                <tr>
                    <th>Date</th>
                    <th>Temp. (C)</th>
                    <th>Temp. (F)</th>
                    <th>Summary</th>
                </tr>
                </thead>
                <tbody>
                {forecasts.map((forecast, index) => (
                    <tr key={index}>
                        <td>{forecast.date}</td>
                        <td align="center">{forecast.temperatureC}</td>
                        <td align="center">{forecast.temperatureF}</td>
                        <td>{forecast.summary}</td>
                    </tr>
                ))}
                </tbody>
            </table>
        );

    return (
        <div>
            <h2 id="tableLabel">Weather forecast</h2>
            <p>This component demonstrates fetching data from the server.</p>
            {contents}
        </div>
    );
};

Давайте рассмотрим этот код:

  • Интерфейс Forecast определяет структуру данных прогноза погоды, которая включает дату, температуру в градусах Цельсия и Фahrenheit, а также резюме погоды. Интерфейс State описывает структуру состояния компонента, состоящего из массива прогнозов погоды и флага загрузки.
  • Компонент WeatherForecast извлекает функцию fetchBff из хука useBff и использует ее для загрузки данных о погоде с сервера. Строка состояния компонента управляется при помощи хука useState, инициализируясь с пустого массива прогнозов и флага загрузки, установленного в true.
  • Хook useEffect вызывает функцию fetchBff, когда компонент монтируется, загружая данные прогноза погоды с сервера с конечной точки /bff/weatherforecast. once the server’s response is received and converted to JSON, the data is stored in the component’s state (via setState), and the loading flag is updated to false.
  • В зависимости от значения флага загрузки, компонент либо отображает сообщение “Loading…”, либоRender a table with the weather forecast data. The table includes columns for the date, temperature in Celsius and Fahrenheit, and a summary of the weather for each forecast.

Теперь добавьте компонент WeatherForecast в BffSample\ClientApp\src\App.tsx:

TypeScript

 

// ******************* START *******************
import { WeatherForecast } from "./components/WeatherForecast";
// ******************** END ********************

// ...

    
// ******************* START *******************
// ******************** END ********************
   

Выполнение и тестирование

Если все было сделано правильно, вы можете теперь запустить все три наших проекта. Используйте консольное сообщение dotnet run -lp https для каждого приложения, чтобы запустить их с использованием HTTPS.

После запуска всех трех приложений, откройте приложение BffSample в вашем браузере (https://localhost:5003) и авторизуйтесь с помощью учетных данных [email protected] и Jd!2024$3cur3. После успешной авторизации вы должны увидеть список заявлений, полученных от сервера аутентификации, как было ранее. Ниже этого у вас также будет виден прогноз погоды.

Прогноз погоды предоставляется отдельным приложением ApiSample, который использует токен доступа, выданный сервисом аутентификации OpenIDProviderApp. Видящий прогноз погоды в окне приложения BffSample указывает, что наш SPA успешно вызвал backend BffSample, который затем проксировал вызов к ApiSample, добавив токен доступа. ApiSample проверил вызов и ответил JSON, содержащим прогноз погоды.

Полное решение доступно на GitHub.

Если в ходе реализации тестовых проектов у вас возникли какие-либо проблемы или ошибки, вы можете обратиться к полной реализации, доступной в репозитории GitHub. просто клонируйте репозиторий Abblix/Oidc.Server.GettingStarted, чтобы получить полностью реализованные проекты, описанные в этой статье. Этот ресурс выступает в качестве инструмента для устранения неполадок и отличного starting point для создания ваших собственных проектов.

Заключение

Эволюция протоколов аутентификации, таких как OAuth 2.0 и OpenID Connect, отражает более широкие тенденции в cybersecurity и возможностях браузеров.Переход от устаревших методов, таких как Implicit Flow, к более безопасным подходам, таким как Authorization Code Flow с PKCE,显著增强了安全性. Однако внеконтролируемый environment operating создает сложности при обеспечении безопасности современных SPA. хранение токенов исключительно на backend и принятие Backend-For-Frontend (BFF) pattern представляет собой стратегию для снижения риска и обеспечения устойчивой защиты данных пользователей.

Разработчики должны оставаться наблюдательными при решении непрекращающейся угрозы введением новых методов аутентификации и актуальных архитектурных подходов. Proactive approach это важно для building secure and reliable web applications. In this article, мы исследовали и реализовали современный подход к интеграции OpenID Connect, BFF и SPA с использованием популярного stack .NET и React технологии. Mы проверили, как эта стратегия может служить мощным foundation для ваших future projects.

В будущем продолжение развития web безопасности потребует еще большей инновации в сфере аутентификации и архитектурных схем. Мы призываем вас исследовать наш репозиторий GitHub, принять участие в разработке современных решений по аутентификации и оставаться в курсе происходящих развитий. Спасибо за внимание!

Source:
https://dzone.com/articles/modern-authentication-on-dotnet