WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

VS2022 .NET 6.0 React のユーザー認証

by WebSurfer 30. March 2022 11:37

Visual Studio 2022 の「React.js での ASP.NET Core」のテンプレートで[フレームワーク(F)]を「.NET 6.0 (長期的なサポート)」とし[認証の種類(A)]を「個別のアカウント」として作成したプロジェクトでユーザー認証に失敗する件につき自分が調べたことと解決策を備忘録として残しておきます。

2022/4/18 追記: 原因は .NET 6.0 Known Issues によるとプロキシのキャッシュとのことです。それに workaround が書いてありますが、この記事の一つ目の解決策と同じになります。詳しくはこの記事の下の方に追記します。

エラーメッセージ

有効な id と password でログインして ASP.NET Core Identity による認証が通っても、Fetch Data をクリックして Web API のデータを取得しようとすると、上の画像のように、

Unhandled Rejection (SyntaxError): Unexpected end of JSON input

・・・というエラーメッセージが出て失敗します。

理由ははっきりしませんが、エラーメッセージやいろいろ試してみた結果から想像するに、Web API のユーザー認証用の JWT トークンに含まれる Issuer と OpenId Connect を呼び出す際に使用する Authority に不整合が出るということのように思われます。

Visual Studio 2022 でフレームワークが .NET 6.0 の React プロジェクトを実行する場合、バックエンドの ASP.NET アプリ (Web API と IdentityServer を含む) に対する要求はプロキシを通るのでその影響があるのかもしれません。(フレームワークを .NET Core 3.1 とした場合は上のような問題は出ませんが、違いはプロキシが使われてないぐらいしかありませんので)

どういう状況かを詳しく書くと以下の通りです。

初期画面ではメニューバーに Home, Counter, Fetch data という 3 つのメニューが表示されており、その中の Fetch data はサーバーの ASP.NET Core Web API からデータを取得して表示するようになっています。

プロジェクトを作成する際[認証の種類(A)]に「個別のアカウント」を選んだ場合、サーバーの Web API には JWT トークンベースの認証が実装され、JWT トークンなしで Web API にアクセスすると HTTP 401 Unauthorized 応答が返ってくるように設定されます。

Visual Studio でアプリを実行し、認証を受けてない状態でメニューバーの Fetch data をクリックするとログイン画面にリダイレクトされます。ログイン画面で id と password を入力して認証を受けると JWT トークンが発行され、Fetch data が Web API にアクセスしてデータを取りに行きますが、その際 JWT トークンが要求ヘッダに含まれるようになります。(先にメニュバーの Login をクリックしてログイン画面でログインしてから Fetch data をクリックしても同様です)

Web API には JWT トークンを持ってアクセスに行くので認証が通って Web API からのデータの取得に成功し下の画像のようにデータが表示されるということになっています。

Fetch data 画面

しかし、.NET 6.0 の場合はこの記事の一番上の画像のエラーとなります。(ちなみに、.NET Core 3.1 の場合は認証が通って上の画面の通りデータが表示されます)

エラーメッセージによると FetchData.js の 60 行目でエラーとなったとのこと。デバッグしてみると HTTP 401 Unauthorized という応答が返ってきています。

デバッグ画面

Fiddler で調べてみると、下の画像の青枠で示した通り JWT トークンは送信されていますが、赤枠で囲った部分に、

www-authenticate: Bearer error="invalid_token", error_description="The issuer 'https://localhost:44495' is invalid"

・・・と表示されているように Issuer が無効とのことです。

Fiddler の画面

JWT トークンに含まれる Issuer 'https://localhost:44495' と何かに不整合が出ているのは間違いないようですが、.NET Core 3.1 では問題ないのに .NET 6.0 で問題が出る理由が分かりませんでした。

調べてみると、Visual Studio 2022 で作成する .NET 6.0 の React アプリは、Viusal Studio から実行すると React 部分は Node.js 開発サーバーでホストされ (ホットリロードが可能になっています)、サーバー側の ASP.NET Core アプリ (Web API と IdentityServer を含む) は IIS Express または Kestrel でホストされるようになっています。

Node.js 開発サーバーと IIS Express (または Kestrel) では必然的にホスト名が異なりクロスドメインの問題が出ます。その問題を解決するために http-proxy-module (HPM) という Node.js のプロキシを使っていて、Visual Studio から .NET 6.0 の React アプリを起動するとプロジェクトに含まれている setupProxy.js の設定に従い以下の通り HPM を起動する画面が出ます。

http-proxy-module (HPM)

上の画像の通り Web API のコントローラーの URL 'weatherforecast' や IdentityServer の APIがプロキシの対象に含まれており、それへの要求を受けると https://localhost:44395 のサーバー (IIS Express) へ渡すように設定されています。

(https://localhost:44395 は launchSettings.json に設定されている IIS Express を使った場合の URL です。Kestrel を使う場合は別のポートになりますが、Visual Studio でアプリを起動する際の設定に従い自動的に切り替わります)

Node.js 開発サーバーの URL はプロジェクトファイルに含まれている SpaProxyServerUrl の設定になるようです。下の画像の通り https://localhost:44495 になっています。

プロジェクトファイル

まとめると、開発環境での URL 'https://localhost:xxxxx' のポート xxxxx は以下のようになります。

ブラウザ (44495) ⇔ Node.js 開発サーバー (44495) ⇔ NPM ⇔ IIS Express (44395)

JWT トークンを発行するのは IIS Express でホストされる IdentityServer ですが、その際 "iss" : "https://localhost:44495" と設定されます。それは JSON Web Tokens - jwt.io で JWT トークンを解析して下の画像のようになっていることで確認しました。(44395 でなく 44495 となっている理由は不明)

JWT トークンを解析

さらに調べていると Microsoft のドキュメント「SPA の認証と承認」の Azure App Service on Linux というセクションに "Linux での Azure App Service デプロイについては、Startup.ConfigureServices に発行者を明示的に指定します" と書いてあって、その対応のためのコードが載っているのを見つけました。

自分の環境には Linux も Azure も関係ないはずですが、試しにそのコードを Program.cs に追加したら問題は解決しました。コメントで「// 追加」とした部分が追加したコードで、それ以外はテンプレートが自動生成した既存のコードです。JwtBearerOptions クラスの Authority プロパティを "https://localhost:44495" に設定しています。

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.EntityFrameworkCore;
using ReactNet6Identity.Data;
using ReactNet6Identity.Models;

// 追加
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.ApiAuthorization.IdentityServer;

var builder = WebApplication.CreateBuilder(args);

// 追加
builder.Services.Configure<JwtBearerOptions>(
    IdentityServerJwtConstants.IdentityServerJwtBearerScheme,
    options =>
    {
        options.Authority = "https://localhost:44495";
    });

// ・・・以下略・・・

Microsoft のドキュメント「JwtBearerOptions.Authority プロパティ」の説明によると、"OpenIdConnect を呼び出す際に使用する Authority を取得または設定します" ということで、上のコードはその Authority を "https://localhost:44495" に設定したということになるはずです。

でも無知な自分には OpenIdConnect って何? Authority って何? ・・・という感じで、なぜ問題が出なくなったのかさっぱり分かりません。(汗)

もう一つ見つけた解決策は、これもなぜ問題が出なくなるのが分かりませんが、IdentityServerOptions クラスの IssuerUri プロパティに "https://localhost:44495" を設定することです。Program.cs の既存のコードに手を加えて以下のようにします。

builder.Services.AddIdentityServer(options => 
    options.IssuerUri = "https://localhost:44495")
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

このような設定をしなくても、そもそもが Issuer は "https://localhost:44495" になるのに、なぜこれが解決になるのか分かりません。想像ですが、明示的に設定すると JwtBearOptions の Authority に影響があるのかもしれません。

React アプリの認証について少し調べてみましたが、Microsoft のドキュメント「SPA の認証と承認」によると、React アプリには ASP.NET Core Identity をユーザー情報のストアとして使う OpenID Connect ベースの認証サーバー Duende Identity Server を使えるように Visual Studio のテンプレートが作られているようです。

では OpenID Connect とは何かですが、「Microsoft ID プラットフォームと OpenID Connect プロトコル」によると、"OpenID Connect (OIDC) は OAuth 2.0 を基盤とした認証プロトコルであり、ユーザーをアプリケーションに安全にサインインさせるために利用できます" とのことです。

ASP.NET Core MVC などに使う ASP.NET Core Identity によるクッキーベースの認証や、先の記事「ASP.NET Core Web API と JWT」で書いたようなトークンベースの単純な認証方式とは違うようだということは何となく分かりました。勉強しなくては・・・


2022/4/18 追記

"The issuer 'https://localhost:44495' is invalid" となる原因が分かりました。.NET 6.0 Known IssuesSPA template issues with Individual authentication when running in development というセクションがあって、それに以下のように書いてありました。

"The first time SPA apps are run, the authority for the spa proxy might be incorrectly cached which results in the JWT bearer being rejected due to Invalid issuer. The workaround is to just restart the SPA app and the issue will be resolved. If restarting doesn't resolve the problem, another workaround is to specify the authority for your app in Program.cs: builder.Services.Configure<JwtBearerOptions>("IdentityServerJwtBearer", o => o.Authority = "https://localhost:44416"); where 44416 is the port for the spa proxy."

要するに、プロキシが Authority (JWT トークンの発行者 = IdentityServer) を間違ってキャッシュするため、送信されてきた JWT トークンに含まれる発行者と一致しなくて、invalid と判定されたということのようです。

.NET 6.0 Known Issues に書いてある workaround は Authority を JWT トークンに含まれる "iss" : "https://localhost:xxxxx" に合わせるということで、この記事に 2 つ書いてある解決策の一つ目と同じです。この記事の例では JWT トークンは "iss" : "https://localhost:44495" となっており、それを Authority に設定しています。

この記事の二つ目の解決策は IdentityServer 側で発行者を "https://localhost:44495" に設定するというものです。これでプロキシのキャッシュの Authority が "https://localhost:44495" になって、JWT トークンの "iss" : "https://localhost:44495" と一致するのではないかと想像してます。

ただ、まだ分からない点があります。それはテンプレートでプロジェクトを作成した状態のデフォルトで、JWT トークンに含まれる "iss" が "https://localhost:44495" になることです。この記事の例では IdentityServer は "https://localhost:44395" なのでそれが Authority になり、IdentityServer が発行した JWT トークンは "iss" : "https://localhost:44395" になるはずなのですが・・・

もう一つ、.NET 6.0 Known Issues に書いてある以下の件ですが、こちらはプロジェクト作成直後の DB が生成されてない状態で登録・ログインしようとすると表示されるはずのメッセージが、プロキシの問題で表示されないというものです。JWT トークンが invalid と判定されるというこの記事の話とは別の問題です。

"When using localdb (default when creating projects in VS), the normal database apply migrations error page will not be displayed correctly due to the spa proxy. This will result in errors when going to the fetch data page. Apply the migrations via 'dotnet ef database update' to create the database."

Tags: , ,

React

About this blog

2010年5月にこのブログを立ち上げました。その後ブログ2を追加し、ここはプログラミング関係、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  December 2022  >>
MoTuWeThFrSaSu
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar