.NET上の modern authentication: OpenID Connect, BFF, SPA

Web技術がどのように進化しているにつれて、それに対応するセキュリティ方法とプロトコルも進化しています。OAuth 2.0とOpenID Connectのプロトコルは、新たなセキュリティ脅威とウェブアプリケーションの複雑さの増加に対応して、大きく進化しました。従来の認証方法は一度有効であったものが、現在、モダンなSPAにとって時代遅れとなっており、新しいセキュリティの挑戦に直面しています。この背景で、Backend-For-Frontend(BFF)のアーキテクチャパターンが推奨される解決策となっており、SPAとバックエンドシステムの間のインタラクションを組織することができ、認証とセッション管理においてより安全で、管理性が高い方法を提供しています。この記事はBFFパターンを深く探索し、.NETとReactを使用した最小限のソリューションを実装して、その実用的な適用を示します。最終的に、BFFパターンをどのように利用してwebアプリケーションのセキュリティと機能を向上させるかを明确に理解することができます。

歴史の背景

OAuth 2.0とOpenID Connectの歴史は、インターネット技術の進行にとって象徴的なものです。これらのプロトコルと、モダンなウェブアプリケーションに与えた影響についてより詳細に見てみましょう。

2012年に導入されたOAuth 2.0プロトコルは、認可において幅広く採用されている標準として進化しました。これにより、第三者のアプリケーションは、クライアントにユーザーの資格情報を露出することなく、ユーザーのリソースに限られたアクセスを取得することができます。OAuth 2.0は、それぞれ、幅広い用途に適したフローをサポートしています。

OAuth 2.0の基礎を建て、2014年にOpenID Connect (OIDC)プロトコルが出現し、基本的な認証機能を追加した。これは、クライアントアプリケーションに標準的な方法を提供して、ユーザーのIDを確認し、JWT (JSON Web Token)形式のIDトークンを取得することで標準化されたアクセスポイントを通じてまたはそれによって基本的な情報を得ることができます。

脅威モデルの進化

SPAの機能が増え、人気が上がるに伴い、SPA用の脅威モデルも進化しました。XSS(Cross-Site Scripting)とCSRF(Cross-Site Request Forgery)などの脆弱性がより一般的になりました。SPAはよくAPIを通じてサーバーとやりとりするため、アクセストークンとリフレッシュトークンを安全に保管して使用することは安全になるために重要です。

時代の要求に応じて、OAuthとOpenID Connectプロトコルは新しい技術に伴う新しい挑戦に适応するために進化し続けています。同時に、脅威の常に進化し、セキュリティの慣習の改善により、古い手法は modern security requirements を満たすことができなくなっています。結果として、OpenID Connectプロトコルは現在、幅広い機能を提供していますが、多くの機能はすでに、またはすぐに、古いものと考えられ、しばしば安全でないと考えられます。この多様性は、SPA開発者にとって、OAuth 2.0とOpenID Connectサーバーとのやりとりを最適かつ安全な方法で行うのに困难的な状況を引き起こします。

特に、隐式フローは今や过时とされており、SPA、モバイルアプリ、デスクトップアプリのいずれものクライアントにとって、認可コードフローを使用して、プロof Key for Code Exchange(PKCE)を伴うことは強く推奨されています。

现代的なSPAの安全性

なぜ、 modern SPAsはPKCEを使用した認可コードフローを使用していても依然として脆弱性があると考えられているのか? この質問にはいくつかの答えがあります。

JavaScriptコードの脆弱性

JavaScriptは強力なプログラミング言語であり、现代的な单一ページアプリケーション(SPA)に关键的な役割を果たしています。しかし、その幅広い機能と一般的な使用は潜在的な危険をもたらしています。React、Vue、Angularなどのライブラリやフレームワークに基づいて構築される现代的なSPAは、 vast numberのライブラリと依存関係を持っています。node_modulesフォルダーにあり、これらの依存関係の数は数百としても、または数千としてもあり得ます。これらのライブラリのそれぞれには、異なる程度の重要性の脆弱性が含まれる可能性があり、SPA開発者は使用されているすべての依存関係のコードを彻底に確認することができません。開発者は通常、それぞれの依存関係の完全なリストを追跡することもできず、それらはお互いに依存関係を持っているためです。自作コードを最高の品質と安全性の標準に開発したとしても、完成したアプリケーションに脆弱性がないかを完全に保証することはできません。

悪意のあるJavaScriptコードは、Cross-Site Scripting(XSS)などの攻撃や第三者のライブラリの侵害を通じてアプリケーションに注入されることができ、正当なアプリケーションコードと同じ権限とデータアクセスレベルを得ることができます。これにより、悪意のあるコードは現在のページからデータを盗むこと、アプリケーションインターフェースとのやり取り、バックエンドにリクエストを送信すること、ローカルストレージ(localStorage、IndexedDB)からデータを盗むこと、甚至に自身の認証セッションを開始し、同じ認可コードとPKCEフローを使用して自身のアクセストークンを取得することができます。

Spectre弱点

Spectre 弱点は、 modern processor architecture の特徴を利用して、 isolation に应收じられるデータにアクセスすることができます。このような脆弱性は、SPAに特に危険です。

まず、SPAはJavaScriptを強く使用して、アプリケーション状態を管理し、サーバとのやり取りを行います。これにより、悪意のあるJavaScriptコードがSpectre脆弱性を利用するための攻撃面を大幅に増やします。また、 tradition multi-page applications(MPA)とは異なり、SPAは再読み込みを少なくするために、ページと読み込まれたコードが長い時間活性に保ちます。これにより、攻撃者は恶意のJavaScriptコードを使用して攻撃を行う時間を大幅に増やすことができます。

Spectre脆弱性により、攻撃者はJavaScriptアプリケーションのメモリに保存されたアクセストークンを盗むことができ、正しいアプリケーションを mimic して保護されたリソースにアクセスすることができます。推論実行をもって、ユーザーセッションデータを盗むこともできます。これにより、SPAが閉じられた後でも攻撃者は攻撃を続けることができます。

未来に、Spectreに似たような他の脆弱性の発見は排除できません。

どうするのか?

重要な中间結論をまとめましょう。モダンなSPAは、多くの第三者のJavaScriptライブラリに依存し、ユーザー装置上のブラウザ環境で実行しています。開発者が完全にコントロールできないソフトウェアとハードウェア環境で作動しています。したがって、これらのアプリケーションは本质的に脆弱であると考えるべきです。

リストされた脅威に対する対策として、多くの専門家が、トークンをブラウザに保存するのを完全に避け、アプリケーションを設計して、アクセストークンとリフレッシュトークンをアプリケーションのサーバー側しか取得して処理しないようにすることに倾斜しています。これらはブラウザ側に渡られることはないようにする。後端とSPAを組み合わせた場合、Backend-For-Frontend (BFF) アーキテクチャパターンを使用することでこれを実現することができます。

認可サーバー(OP)、BFFパターンを実装しているクライアント(RP)、第三者のAPI(リソースサーバー)の間のやり取りのスケームは以下のようになります:

BFFパターンを使用してSPAを保護することには、いくつかの利点があります。アクセストークンとリフレッシュトークンはサーバー側に保存され、ブラウザ側に渡られることはないため、脆弱性による盗難が防止されます。セッションとトークンの管理はサーバーで行われ、より良いセキュリティ控制在とより信頼性のある認証検証が可能です。クライアントアプリケーションはBFFを通じてサーバーとやり取りすることで、アプリケーションのロジックを簡略化し、悪意のコード実行の危険性を减少することができます。

.NET プラットフォームでBackend-For-Frontend パターンを実装する。

私たちは.NETプラットフォームでBFFの実践的な実装に進む前に、必要なコンポーネントを考慮して行動計画を練る必要があります。既に設定されたOpenID Connectサーバーを持っていると仮定し、OpenID Connectを使用した認証を実装し、BFFパターンを使用してサーバーとクライアントの部分間の対話を組織する必要があるSPAを開発することを考えます。

文書OAuth 2.0 for Browser-Based Applicationsによると、BFFアーキテクチャパターンは、バックエンドがOpenID Connectクライアントとして行動し、認可コードフローを使用して認証し、PKCEを使用して、アクセストークンとリフレッシュトークンを自身側で取得および保存し、ブラウザ内でSPA側に渡すことはないと仮定しています。BFFパターンはまた、バックエンド側にAPIが存在し、以下の4つの主要なエンドポイントで構成されていることを想定しています。

  1. Check Session: アクティブなユーザー認証セッションを確認するために使用されます。通常、SPAから非同期API(fetch)を使用して呼び出され、成功すると、アクティブなユーザーに関する情報を返します。したがって、CDNなど第三の源から読み込まれたSPAは、認証状態を確認し、ユーザーとの作業を続けるか、OpenID Connectサーバーを使用して認証を行うか決めることができます。
  2. ログイン: OpenID Connect サーバー上で認証プロセスを開始します。通常、SPAは第1ステップで Check Sessionを通じて認証されたユーザーデータを取得できない場合、このURLにブラウザをリダイレクトし、これによりOpenID Connect サーバーに完全なリクエストを形成し、ブラウザをそこにリダイレクトします。
  3. サインイン: 認証に成功した場合、第2ステップの後にサーバーから送られる認可コードを受け取ります。認可コードとPKCE コード検証器を交換してアクセスとリフレッシュ トークンを取得するために、OpenID Connect サーバーに直接リクエストを送信します。ユーザーに認証クッキーを発行して、クライアント側で認証されたセッションを開始します。
  4. ログアウト: 認証セッションを終了するための機能を提供します。通常、SPAはこのURLにブラウザをリダイレクトし、これによりOpenID Connect サーバーのEnd Session エンドポイントにリクエストを形成し、セッションを終了します。また、クライアント側のセッションと認証クッキーも削除します。

次に、.NET プラットフォームが提供しているツールを調査し、BFF パターンの実装に使用できるのを確認しましょう。.NET プラットフォームは Microsoft.AspNetCore.Authentication.OpenIdConnect NuGet パッケージを提供しています。これは、MicrosoftがサポートするOpenID Connect クライアントの既製実装です。このパッケージは認可コードフローとPKCEを両方サポートし、すでに必要なサインイン エンドポイント機能を実装した相対パス /signin-oidc のエンドポイントを追加します。したがって、残りの3つのエンドポイントだけを実装する必要があります(上記のポイント 3 を参照)。

実用的な統合の例として、Abblix OIDC Serverライブラリーに基づくテストのOpenID Connect サーバーを取り上げます。しかし、以下に触れているすべての内容は、Facebook、Google、Appleなどの公的に利用可能なサーバーにも、OpenID Connect プロトコル仕様に準拠する他のすべてのサーバーにも適用されます。

前端侧でSPAを実装するために、React ライブラリーを使用し、後端側では.NET WebAPIを使用します。これは、本文が書かれたときの最も一般的な技術スタックの1つです。

コンポーネントとその相互作用の全体的なスchemeは以下のようになっています。

この記事の例を実行するためには、.NET SDKNode.jsもインストールする必要があります。この記事のすべての例は、.NET 8、Node.js 22、React 18で開発およびテストされています。これらは、記事を書いたときの最新のバージョンです。

Reactを使用したClient SPAの作成と.NET上のバックエンドでの実装

クライアントアプリケーションを迅速に作成するために、既に用意されているテンプレートを使用するのは便利です。.NET 7までのバージョンで、SDKは.NET WebAPIアプリケーションとReact SPAのための内蔵テンプレートを提供していました。残念ながら、このテンプレートは.NET 8のバージョンで削除されました。この理由で、Abblixチームは自作のテンプレートを作成しました。これには.NET WebApiのバックエンド、Reactライブラリを基盤とするフロントエンドSPA、TypeScript、Viteを使用して作成されたものが含まれています。このテンプレートはAbblix.Templatesパッケージの一部として公開されていて、以下のコマンドを実行してインストールすることができます。

Shell

 

dotnet new install Abblix.Templates

今では、名前がabblix-reactのテンプレートを使用できます。これを使用して新しいアプリケーションBffSampleを作成してみましょう。

Shell

 

dotnet new abblix-react -n BffSample

このコマンドは、.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に変更します。

次に、BffSample フォルダに移動し、OpenID Connect クライアントを実装する NuGet パッケージを追加するために、以下のコマンドを実行します。

Shell

 

dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

アプリケーションで、名前を CookiesOpenIdConnect として2つの認証スキーマを設定し、アプリケーション設定からそれぞれの設定を読み取ります。これを行うには、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:
    • SignInSchemeSignOutSchemeのプロパティは、サインイン後に使用者情報を保存するために使用されるCookiesスchemeを指定します。
    • Authorityプロパティは、OpenID Connect サーバの基本 URL を含みます。ClientIdClientSecretは、OpenID Connect サーバに登録されたクライアントアプリケーションの識別子と秘密鍵を指定します。
    • SaveTokensは、OpenID Connect サーバから取得されたトークンを保存する必要を示します。
    • Scopeは、BffClientアプリケーションがアクセスを要求するスコープのリストを含みます。この場合、標準のスコープopenid(使用者識別子)、profile(使用者プロフィール)およびemail(電子メール)が要求されています。
    • MapInboundClaimsは、OpenID Connect サーバからの入力CLAIMSをアプリケーションで使用するCLAIMSに変換することを責任とします。falseの値は、CLAIMSがOpenID Connect サーバから受信された形式で認証されたユーザーのセッションに保存されることを意味します。
    • ResponseTypeの値codeは、クライアントがAuthorization Code Flowを使用することを示します。
    • ResponseModeは、Authorization CodeFlowのデフォルトの方法である、Authorization Codeをクエリストリングに送信することを指定します。
    • UsePkceプロパティは、認証过程中でAuthorization Codeの盗難を防止するためにPKCEを使用する必要を示します。
    • GetClaimsFromUserInfoEndpointプロパティは、UserInfo エンドポイントから使用者プロファイルデータを取得する必要を示します。

私たちのアプリケーションは、認証なしでユーザーとのやり取りがないと考えていますので、React SPA は認証成功後にだけ読み込まれることを保証します。もちろん、SPA は Content Delivery Network (CDN) のサーバーや npm start コマンドで起動されるローカル開発サーバーなど、外部源から読み込まれる可能性があります。このような場合、SPA の読み込み前に認証状態を確認することはできません。しかし、自作の .NET バックエンドが SPAs の読み込みを責任としている場合、可能です。

これを行うために、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 サーバーの設定を行います。

前述のように、実用的な統合例を説明するために、Abblix OIDC Server ライブラリに基づくテストの OpenID Connect サーバーを使用する。ASP.NET Core MVC に基づくアプリケーションの基本テンプレートは、先ほどインストールした `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);

// ...

// Abblix OIDC Serverを登録および設定する
builder.Services.AddOidcServices(options =>
{
    // 以下の場所でOIDCサーバーの設定を行います。
    // ******************* 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 ********************
    // 以下のURLは、AuthControllerのLoginアクションにリンクしています。
    options.LoginUri = new Uri($"/Auth/Login", UriKind.Relative);

    // 以下の行は、トークン署名用の新しいキーを生成します。独自のキーを使用したい場合は、それを更换してください。
    options.SigningKeys = new[] { JsonWebKeyFactory.CreateRsa(JsonWebKeyUseNames.Sig) };
});

詳細にこのコードを再び確認しましょう。bff_sampleという識別子でクライアントを登録し、secretという秘密鍵(SHA512のハッシュとして保存)を設定します。これにより、トークンの取得時には、POSTメッセージで送信される秘密鍵を使用したクライアント認証を行うことを示しています(ClientAuthenticationMethods.ClientSecretPost)。AllowedGrantTypesは、クライアントが使用できるのはAuthorization Code Flowだけであることを指定します。ClientTypeは、クライアントを機密性のあるものと定義し、これは秘密鍵を安全に保管することができることを意味します。OfflineAccessAllowedは、クライアントがrefresh tokenを使用することを許可します。PkceRequiredは、認証过程中にPKCEを使用することを強制します。RedirectUrisPostLogoutRedirectUrisは、認証後やセッション終了後にredirectされる許可されたURLのリストを含んでいます。

他のOpenID Connect サーバーにおいても、設定は似たようなものであり、その違いは設定方法だけに限ります。

BFF APIの基本的な実装

以前に触れたように、Microsoft.AspNetCore.Authentication.OpenIdConnectパッケージの使用は、サンプルアプリケーションにSign In のエンドポイントの実装を自動的に追加します。今度は、BFF APIの残りの部分を実装する時間です。これらの追加のエンドポイントにはASP.NET MVC コントローラーを使用します。まず、Controllersフォルダーを追加し、BffSampleプロジェクトにBffController.csというファイルを作成し、以下のコードをそこに入れます。

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();
    }
}

このクラスコードを詳細に分割しましょう:

  • [Route("[controller]")]属性は、コントローラ内のすべてのアクションの基本ルートを設定します。この場合、ルートはコントローラの名前にマッチするため、私たちのAPIメソッドにアクセスするすべてのパスは/bff/から始まります。
  • 定数CorsPolicyName = "Bff"は、CORS(クロスオリジンリソース共有)ポリシーの名前を定義して、メソッド属性で使用します。後で参照します。
  • 3つのメソッドCheckSessionLoginLogoutは、上記の必要なBFF機能を実装します。それぞれ/bff/check_session/bff/login/bff/logoutでGETリクエスト、POSTリクエストを処理します。
  • CheckSessionメソッドは、ユーザーの認証状態を確認します。認証されていない場合、401 Unauthorizedコードを返却し、SPAに認証エンドポイントにリダイレクトすることを期待します。認証が成功した場合、このメソッドは証明書とその値のセットを返します。このメソッドには、認証に使用されるクッキーを含むクロスドメインの呼び出しを想定して、名前CorsPolicyNameのCORSポリシーバインディングが含まれます。
  • Login メソッドは、前の CheckSession 呼び出しが 401 Unauthorized を返却した場合、SPA によって呼び出されます。これは、ユーザーがまだ認証されていないことを保証し、設定された Challenge プロセスを開始します。これにより、OpenID Connect サーバーにリダイレクトされ、Authorization Code Flow と PKCE を使用してユーザー認証を行い、認証クッキーを発行します。これの後、コントロールは私たちのアプリケーションのルート "~/" に返り、SPA が再読み込まれ、認証されたユーザーで始動します。
  • Logout メソッドも SPA によって呼び出されますが、現在の認証セッションを終了します。これは、BffSampleのサーバー側から発行された認証クッキーを削除し、OpenID Connect サーバー側の End Session エンドポイントにも呼び出します。

BFF 用 CORS の設定

上記に述べたように、CheckSession メソッドは、SPAからの非同期呼び出し(通常は Fetch API を使用)用に設計されています。このメソッドの適切な機能は、ブラウザから認証クッキーを送信する能力に依存します。SPAが別の静的ウェブホスト(例:CDNや別のポートで稼働している開発サーバ)から読み込まれる場合、この呼び出しはクロスドメインになります。これは、CORS ポリシーの設定が必要で、その無い場合、SPAはこのメソッドを呼び出すことができません。

私たちは、Controllers\BffController.cs ファイルのコントローラコードで、CorsPolicyName = "Bff" という名前の CORS ポリシーを使用することを示しています。これまでには、このポリシーのパラメーターの設定を行わなかったので、今は BffSample/Program.cs ファイルに以下のコードブロックを追加します。

C#

 


// ******************* 開始 *******************
using BffSample.Controllers;
// ******************** 終了 ********************
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();

// ...

builder.Services
    .AddAuthorization()
    .AddAuthentication(options => configuration.Bind("Authentication", options))
    .AddCookie()
    .AddOpenIdConnect(options => configuration.Bind("OpenIdConnect", options));
// ******************* 開始 *******************
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();
        }));
// ******************** 終了 ********************
var app = builder.Build();

このコードでは、CORSポリシーの方法は、CorsSettings:AllowedOriginsとして指定された配置の文字列の配列からSPAを読み込むために呼び出すことができます。GETメソッドを使用して、このコールに COOKIE を送信することができます。また、app.UseCors(...)の呼び出しはapp.UseAuthentication()のすぐ前に置かれることを確認してください。

C#

 

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// ******************* 開始 *******************
app.UseCors(BffController.CorsPolicyName);
// ******************** 終了 ********************
app.UseAuthentication();
app.UseAuthorization();

CORSポリシーを正しく機能させるために、BffSample\appsettings.Development.json設定ファイルに対応する設定を追加します。

JSON

 

{
  // ******************* 開始 *******************
  "CorsSettings": {
    "AllowedOrigins": [ "https://localhost:3000" ]
  },
  // ******************** 終了 ********************
 "OpenIdConnect": {
   "Authority": "https://localhost:5001",
   "ClientId": "bff_sample",

例えば、https://localhost:3000はReact SPAをnpm run devコマンドで起動するためのdevサーバーの場所です。このアドレスはBffSample.csprojファイルを開いてSpaProxyServerUrlパラメーターの値を探すことで、おそらく見つかります。実際のアプリケーションでは、CORSポリシーにCDN(コンテントデリvery Network)またはそのようなサービスのアドレスを含めることができます。SPAがBFF APIから異なるアドレスとポートで読み込まれる場合、このアドレスをCORSポリシー設定に追加する必要があります。

BFFを使用した認証の実装

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関数には認証に必要なcookiesを処理するためのクレデンシャルが含まれる
            return await fetch(`${baseUrl}/${endpoint}`, {
                credentials: 'include',
                ...options
            });
        } catch (error) {
            console.error(`Error during ${endpoint} call:`, error);
            throw error;
        }
    };

    // ログイン関数は、ユーザーに認証が必要である場合にログイン画面にリダイレクトする
    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);
        }
    };

    // ユーザーをlogoutする関数
    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);

// BFFコンテキストにアクセスするためのHOCをエクスポートする
export const withBff = (Component: React.ComponentType) => (props: any) =>
    
        {context => }
    ;

このファイルは以下をエクスポートしています:

  • <code>BffProvider</code>コンポーネント、これはBFF用のコンテキストを作成し、アプリケーション全体に認証とセッション管理に関連する機能と状態を提供します。
  • current user stateとBFFとの操作を行うための機能を持ったオブジェクトを返すカスタムフックuseBff()。 functional React componentsで使用することが意図されている。
  • class-based React componentsで使用するためのHigher-Order Component (HOC) withBff

次に、認証成功後、現在のユーザーのCLAIMSを表示するUserClaims componentを作成します。BffSample\ClientApp\src\componentsフォルダにUserClaims.tsxファイルを作成し、以下の内容を入力してください。

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()フックを使用して認証されたユーザーを確認し、認証された場合はユーザーのCLAIMSをリスト形式で表示します。また、ユーザーのデータがまだ利用できない場合は、Checking user session...というテキストを表示します。

次に、BffSample\ClientApp\src\App.tsxファイルに移行します。必要なコードに置き換えます。components/Bff.tsxからBffProviderをインポートし、components/UserClaims.tsxからUserClaimsをインポートし、主要なコンポーネントのコードを插入します。

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パラメーターは、私のBFF APIの基本URLhttps://localhost:5003/bffを指定しています。これは単純化されたものであり、簡略化されています。実際のアプリケーションでは、この設定を動的に提供する必要があります。これを実現する方法はいくつかありますが、その詳細はこの記事の範囲を超えます。

Logout ボタンは、ユーザーをログアウトさせるためのボタンです。これはuseBffフックを通じて利用可能なlogout関数を呼び出し、ユーザーのブラウザーを/bff/logoutエンドポイントにリダイレクトし、サーバー側でユーザーのセッションを終了します。

この段階で、BffSampleアプリケーションをOpenIDProviderAppと一緒に実行することができます。各プロジェクトにあるコマンドdotnet run -lp httpsを使用して、それぞれのプロジェクトやお好みのIDEから起動することができます。どちらのアプリケーションも同時に実行する必要があります。

これの後、ブラウザーを開いてhttps://localhost:5003に移動します。もしすべてが正しく設定されているなら、SPAが読み込まれると/bff/check_sessionを呼び出します。/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]

また、Logoutボタンをクリックすると、ブラウザーは/bff/logoutにリダイレクトされ、ユーザーがログアウトされ、ログイン画面にもどり、ユーザー名とパスワードを入力する提示が表示されます。

Abblix/Oidc.Server.GettingStartedにあるGitHubリポジトリとあなたのコードを比較することができます。このリポジトリには、この例と他の実行準備ができた例が含まれています。

HTTPS証明書の信頼問題の解決

HTTPSを使用して動作するWebアプリケーションをローカルでテストする際に、SSL証明書が信頼されていないというブラウザの警告が発生することがあります。この問題は、ASP.NET Coreによって使用される開発用証明書が認識された証券発行機(CA)に発行されていないため、自署名またはシステムに存在しないために発生します。これらの警告は、以下のコマンドを一度実行することで消除できます。

Shell

 

dotnet dev-certs https --trust

このコマンドは、localhost用の自署名証明書を生成し、システムにインストールし、この証明書を信頼するようにします。この証明書は、ASP.NET CoreによってローカルでWebアプリケーションの実行に使用されます。このコマンドを実行した後、変更を有効にするためにブラウザを再起動する必要があります。

Chromeユーザーの特に注意:開発証明書を信頼することでも、Chromeのいくつかのバージョンは、安全性のためにlocalhostサイトにアクセスを制限するかもしれません。localhostにアクセスがブロックされ、Chromeによる接続は安全でないと示されるエラーが発生した場合、以下のようにこれらの問題を回避することができます。

  • エラーページのどこかにクリックし、thisisunsafeまたはbadideaと入力してください。これらのキーストロークシーケンスは、Chromeでの回避コマンドとして機能し、localhostサイトにアクセスできるようにします。

開発環境にてのみこれらの回避方法を使用することが重要であることに注意してください。これらは本格的な安全风险を引き起こす可能性があります。

BFFを通じた第三者APIの呼び出し

私たちはBffSampleアプリケーションで認証を成功させました。今は、アクセストークンが必要な第三者APIの呼び出しに移行しましょう。

必要なデータを提供する別のサービスがあると考えてください。例えば、天気予報などで、アクセストークンを持っていないと访问できません。BffSampleのサーバー側は、リバースプロキシとして機能し、SPAからのデータ请求を受け取り、認証し、アクセストークンを追加し、この要求を天気サービスに転送し、サービスからの応答をSPAに返すように機能します。

ApiSampleサービスの作成

BFFを通じたリモートAPIの呼び出しを示す前に、このAPIの役割を果たすアプリケーションを作成する必要があります。

アプリケーションを作成するためには、.NETから提供されるテンプレートを使用します。OpenIDProviderAppBffSampleプロジェクトを含むフォルダに移動し、以下のコマンドを実行してApiSampleアプリケーションを作成します。

Shell

 

dotnet new webapi -n ApiSample

このASP.NET Core Minimal APIアプリケーションは、JSON形式で天気データを提供する/weatherforecastの单一のエンドポイントをサービスします。

まず、ApiSample 应用程序在本地で使用するランダムに割り振られたポート番号を固定のポート 5004に変更します。前述の通り、このステップは必須ではありませんが、設定を簡素化するためには便利です。これを行うためには、ApiSample\Properties\launchSettings.json ファイルを開き、名前が https 的なプロファイルを見つけ、applicationUrl プロパティの値を https://localhost:5004 に変更します。

次に、ウェather APIをアクセストークンだけでアクセス可能にしましょう。ApiSample プロジェクトフォルダに移動し、JWT Bearer トークン認証用のNuGetパッケージを追加してください。

Shell

 

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

次に、ApiSample\Program.cs ファイルに WeatherApi という認証スキーマと認可ポリシーを設定しましょう。

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();

このコードブロックは、アプリケーション設定から configuration を読み取り、JWT(JSON Web Tokens)を使用した認可を含む認証を設定し、名前が WeatherApi の認可ポリシーを設定します。WeatherApi 認可ポリシーは以下の要求を設定します。

  • policy.RequireAuthenticatedUser(): 保護されたリソースにアクセスするには、認証されたユーザーだけがアクセスできることを保証します。
  • policy.RequireAssertion(context => ...): ユーザーにはscope声明が含まれている必要があり、scope声明の値にweatherが含まれていることが必要です。scope声明はRFC 8693に基づいて、空白で区切られた複数の値を含むことができるため、実際のscopeの値は個別の部分に分割され、結果の配列は必要なweatherの値を含むことを確認します。

これらの条件が合致すると、weatherスコープに認証されたアクセストークンを持つユーザーだけが、このポリシーによって保護されるエンドポイントを呼び出すことができます。

私たちは/weatherforecastエンドポイントにこのポリシーを適用する必要があります。以下のようにRequireAuthorization()の呼び出しを追加します。

C#

 

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

// ...

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

認証スキームに必要な設定をApiSampleアプリケーションのappsettings.Development.jsonファイルに追加します。

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",

設定内容を詳細に確認しましょう。

  • Authority: これはJWTトークンを発行するOpenID Connect認証サーバーのURLです。ApiSampleアプリケーションで設定された認証プロバイダは、このURLを使用してトークンを検証するために必要な情報を取得します。たとえば署名キーなどです。
  • MapInboundClaims: この設定は、JWTトークンからの入力CLAIMがASP.NET Core内部のCLAIMにどのように対応されるかを制御します。これはfalseに設定されており、これはCLAIMがJWTからの元の名前を使用することを意味します。
  • TokenValidationParameters:
    • ValidTypes: at+jwtに設定されており、RFC 9068 2.1に基づいて、これはJWT形式のアクセストークンを意味します。
    • ValidAudience: このアプリケーションがhttps://localhost:5004によって発行されたトークンを受け取ることを指定します。
    • ValidIssuer: このアプリケーションがhttps://localhost:5001によって発行されたトークンを受け取ることを指定します。

OpenIDProviderAppの追加設定

認証サービスOpenIDProviderAppとクライアントアプリケーションBffSampleの組み合わせは、ユーザー認証の提供に适しています。しかし、リモートAPIにアクセスするためには、OpenIDProviderAppのリソースとしてApiSampleアプリケーションを登録する必要があります。この例では、RFC 8707: OAuth 2.0のリソース指標をサポートするAbblix OIDC Serverを使用しています。したがって、ApiSampleアプリケーションをスコープweatherとしてリソースとして登録します。他のOpenID Connect サーバーを使用している場合、Resource Indicatorsをサポートしていない場合でも、このリモートAPIに独自のスコープを登録する推奨があります(この例ではweather)。

以下のコードをOpenIDProviderApp\Program.csファイルに追加します。

C#

 

// Abblix OIDC Serverを登録し、構成する
builder.Services.AddOidcServices(options => {
    // ******************* START *******************
    options.Resources =
    [
        new(new Uri("https://localhost:5004", UriKind.Absolute), new ScopeDefinition("weather")),
    ];
    // ******************** END ********************
    options.Clients = new[] {
        new ClientInfo("bff_sample") {

この例では、ApiSample アプリケーションを登録し、その基本アドレス https://localhost:5004 をリソースとして指定し、名前を weather にした特定のスコープを定義します。実際のアプリケーションでは、特に多くのエンドポイントを構成する複雑なAPIにおいて、各個別のエンドポイントまたは関連したエンドポイントのグループに個別のスコープを定義することが推奨されます。この取り組みは、より正確なアクセス制御を可能にし、アクセス権の管理を柔軟に行うことができます。たとえば、異なる操作、アプリケーションモジュール、またはユーザーアクセスレベルに独自のスコープを作成することができます。これにより、APIの特定の部分にアクセスできるユーザーにより細粒度の控制在ります。

BffSampleのリモートAPIにリクエストを代理する機能の詳細

クライアントアプリケーション BffSample は、ApiSample にアクセストークンを要求するだけでなく、SPAからのリクエストを処理する必要があります。これには、OpenIDProviderApp サービスから取得したアクセストークンをこれらのリクエストに追加し、リモートサーバーに送信し、サーバーからの応答をSPAに返す必要があります。基本的に、BffSample はリバースプロキシサーバーとして機能する必要があります。

クライアントアプリケーションでリクエストの代理を手動で実装する代わりに、マイクロソフトによって開発された既成品であるYARP (Yet Another Reverse Proxy)を使用します。YARPは.NETで書かれたリバースプロキシサーバーであり、NuGetパッケージとして利用可能です。

BffSample アプリケーションでYARPを使用するには、まず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() は、DIコンテナに必要なYARPサービスを登録します。
  • app.MapForwarder は、リクエストを別のサーバーやエンドポイントに転送するように設定します。
  • "/bff/{**catch-all}" は、リバースプロキシが応答するパスパターンです。すべての /bff/ で始まるリクエストはYARPによって処理されます。{**catch-all} は、/bff/ の後のURLの残りの部分をすべてキャプチャするために使用されます。
  • configuration.GetValue<string>("OpenIdConnect:Resource") は、アプリケーションの設定を使用して OpenIdConnect:Resource セクションから値を取得します。この値はリクエストが転送されるリソースアドレスを指定します。この例では、この値は https://localhost:5004 となり、ApiSample アプリケーションが動作するベースURLです。
  • builderContext => ...は、YARPがSPAからの各要求に対して行う必要のある変換を追加します。私たちの場合、これらの変換が2つあります。
    • builderContext.AddPathRemovePrefix("/bff")は、元の要求パスから/bffの接頭辞を削除します。
    • builderContext.AddRequestTransform(async transformContext => ...)は、以前認証時に取得したアクセストークンを含むAuthorization HTTPヘッダを要求に追加します。このため、SPAからリモートAPIに向かう要求は、SPA自身がこのトークンにアクセスできないにも関わらず、アクセストークンを使用して認証されます。
  • .RequireAuthorization()は、すべての転送要求に認証が必要であることを指定します。認証されたユーザーのみが/bff/{**catch-all}のルートにアクセスできます。

認証時にリソース https://localhost:5004 のアクセストークンを要求するには、BffSample/appsettings.Development.json ファイルの OpenIdConnect 設定に Resource パラメーターを追加し、値を https://localhost:5004 に設定します。

JSON

 

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

また、BffSample/appsettings.json ファイルの scope 配列に weather を他の値と共に追加します。

JSON

 

{
  "OpenIdConnect": {

    // ...

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

    // ...

  }
}

Notes: 実際のプロジェクトでは、アクセストークンの期限切れを監視する必要があります。トークンが期限切れに近づいている際、認証サービスからリフレッシュトークンを使用して新しいトークンを要求するか、リモートAPIからのアクセス拒否エラーを処理し、新しいトークンを取得して元の要求を再試行する必要があります。この記事では、この侧面を故意に省略しています。

SPA 应用程序中通过 BFF 请求天气 API

後端は準備完了です。ApiSample 应用程序はトークンベースの認可を実装した API を提供し、BffSample 应用程序はこの API に安全なアクセスを提供するための組み込まれたリバースプロキシサーバーを含みます。最後の手順は、React SPA 内でこの API を要求し、取得したデータを表示する機能を追加することです。

次の内容をBffSample\ClientApp\src\componentsにあるWeatherForecast.tsxファイルに追加してください。

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インターフェースは、日付、セルシウスとファーヘンハイトの温度、そして天気の要約を含む天気予報データの構造を定義します。Stateインターフェースは、天気予報の配列と読み込み中のフラグを含むコンポーネントの状態構造を記述します。
  • WeatherForecastコンポーネントは、useBffフックからfetchBff関数を取得し、その関数を使用してサーバから天気データを取得します。このコンポーネントの状態はuseStateフックを使用して管理され、予報の空の配列と読み込み中のフラグをtrueに初期化します。
  • useEffectフックは、コンポーネントがマウントされるとfetchBff関数をトリガーし、/bff/weatherforecastエンドポイントから天気予報データをサーバから取得します。サーバの応答が受信され、JSONに変換された後、データはsetStateを通じてコンポーネントの状態に保存され、読み込み中のフラグはfalseに更新されます。
  • 読み込み中のフラグの値に応じて、コンポーネントは”Loading…”メッセージを表示したり、天気予報データによるテーブルをレンダリングしたりします。テーブルには、日付、セルシウスとファーヘンハイトの温度、そして各予報の天気の要約の列が含まれます。

次に、BffSample\ClientApp\src\App.tsxWeatherForecastコンポーネントを追加してください。

TypeScript

 

// ******************* 開始 *******************
import { WeatherForecast } from "./components/WeatherForecast";
// ******************** 終了 ********************

// ...

    
// ******************* 開始 *******************
// ******************** 終了 ********************
   

実行とテスト

すべてが正しく行われたら、今では3つのプロジェクトをすべて開始することができます。各アプリケーションをHTTPSで実行するには、コンソールコマンドdotnet run -lp httpsを使用してください。

3つのアプリケーションを全て起動した後、ブラウザでBffSampleアプリケーションを開きます(https://localhost:5003)。そして、[email protected]Jd!2024$3cur3の資格情報で認証します。認証に成功すると、以前に見たように認証サーバから受信した声明のリストが表示されます。この下に、天気予報も表示されます。

天気予報は、認証サービスOpenIDProviderAppから発行されたアクセストークンを使用している別のアプリケーションApiSampleによって提供されます。BffSampleアプリケーションの窓で天気予報を見ることができることは、SPAがBffSampleのバックエンドを成功に呼び出し、それがApiSampleにアクセストークンを追加してプロキシングすることを示しています。ApiSampleは呼び出しを認証し、天気予報を含むJSONを返信します。

完整的なソリューションはGitHubにあります。

テストプロジェクトの実装中に問題やエラーに直面した場合、GitHubリポジトリに格納されている完全なソリューションを参照することができます。この記事で説明されている完全に実装されたプロジェクトにアクセスするには、Abblix/Oidc.Server.GettingStartedのリポジトリをクローンするだけです。このリソースは、トラブルシュootingツールとしても、自作プロジェクトの立ち上げのための坚实基础としても機能します。

結論

OAuth 2.0やOpenID Connectなどの認証プロトコルの進化は、webセキュリティとブラウザ機能のより一般的なトレンドを反映しています。Implicit Flowのような過剰な方法から、PKCEを使用したAuthorization Code Flowなどのより安全性の高い方法に移行することで、安全性を大幅に向上させました。しかし、未管理な環境での操作において固有の脆弱性があるため、 modern SPAsのセキュリティを保証するのは難しい課題です。トークンをバックエンドに専用に保管し、Backend-For-Frontend (BFF) パターンを採用することは、リスク軽減と強いユーザーデータ保護を実現する効果的な戦略です。

開発者は、新しい認証方法や最新のアーキテクチャーを実装することで、常に変化する脅威のLayoutに警戒を持ち続けなければならないことに注意してください。この積極的な取り組みは、セキュリティの高いと信的なwebアプリケーションを構築するために非常に重要です。この記事では、人気の.NETとReact技術スタックを使用して、OpenID Connect、BFF、SPAを現代的な方法で統合することを調査し実装しました。この取り組みは、将来のプロジェクトにおいて強力な基础を提供することができます。

将来的に、ウェブセキュリティの持続的な進化は、認証とアーキテクチャパターンに対するさらなる革新を必要とするでしょう。私たちは、GitHubリポジトリを探索し、现代の認証ソリューションの開発に貢献し、進行を追っていくことをお勧めします。お気づきいただき、ありがとうございます!

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