WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

IdentityServer で Web API の認証を サポート

by WebSurfer 2022年4月8日 16:48

先の記事「Duende IdentityServer」で作成した認証サーバーを利用して ASP.NET Core Web API アプリのユーザー認証ができるようにしてみます。

Duende Software のドキュメント Protecting an API using Client Credentials に例が載っていますが、その記事では Machine to Machine で(即ち、個々のユーザー認証なしで)トークンを取得しているところを、IdentityServer に登録済みのユーザーの id と password で認証を受けてトークンを取得するように変更しました。

個々のユーザーのクレデンシャルを使うのは望ましくないというような意味のことがドキュメントに書いてあった気がしますが、せっかく苦労してサンプルを作ったので以下にその手順を書いておきます。

(1) Web API プロジェクトの作成

Visual Studio 2022 の「ASP.NET Core Web API」のテンプレートを使って[フレームワーク(F)]を「.NET 6.0 (長期的なサポート)」とし[認証の種類(A)]を「なし」にして Web API プロジェクトを作成します。

Web API プロジェクトの作成

(2) NuGet パッケージのインストール

Microsoft.AspNetCore.Authentication.JwtBearer を NuGet からインストールします。

NuGet パッケージのインストール

(3) JWT 認証スキーマを登録

Web API プロジェクトに含まれる Program.cs のコードを編集して JWT 認証スキーマを登録します。Duende Software のドキュメントの Adding an APIAuthorization at the API を参考にしました。

.NET 6.0 のプロジェクトの Program.cs では以下のようになります。自動生成されたコードに「// 追加」とコメントしたコードを追加します。

// 追加
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder(args);

// 追加
builder.Services.AddAuthentication(
        JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.Authority = "https://localhost:5001";
            options.TokenValidationParameters = 
                new TokenValidationParameters
                {
                    ValidateAudience = false
                };
        });

// 追加
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ApiScope", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireClaim("scope", "scope3");
    });
});

// ・・・中略・・・

// 追加
app.UseAuthentication();

app.UseAuthorization();
app.MapControllers();
app.Run();

上のコードで、options.Authority に設定する URL は先に作成した Duende IdentityServer の URL に合わせてください。デフォルトでは上のように "https://localhost:5001" となっているはずです。

policy.RequireClaim("scope", "scope3") の "scope3" は後で Duende IdentityServer プロジェクトの Config.cs ファイルで設定します。下のステップ (5) のコードを見てください。

(4) [Authorize] 属性を付与

自動生成された WeatherForecastController コントローラの Get() メソッドに [Authorize] 属性を付与します。using Microsoft.AspNetCore.Authorization; の追加を忘れないようにしてください。

ここまでの設定で JWT トークンベースのアクセス制限の実装は完了しており、トークンなしで WeatherForecastController コントローラの Get() メソッドを要求すると HTTP 401 Unauthorized 応答が返ってくるはずですので試してみてください。

(5) IdentityServer に Client を追加

先に作成済の Duende IdentityServer プロジェクトの Config.cs ファイルを開いて上の Web API アプリの認証をサポートするための Client を追加します。以下の「// 追加」とコメントしたコードを既存のファイルに追加します。

using Duende.IdentityServer.Models;

namespace DuendeIdentityServer
{
    public static class Config
    {

        // ・・・中略・・・

        public static IEnumerable<ApiScope> ApiScopes => new ApiScope[] {
            new ApiScope("scope1"),
            new ApiScope("scope2"),
            
            // 追加
            new ApiScope("scope3")
        };

        public static IEnumerable<Client> Clients => new Client[] {

            // ・・・中略・・・
            
            // 追加
            new Client
            {                
                ClientId = "WebApiNet6",
                ClientSecrets = { new Secret("0C86E143-30E0-4FB4-8710-008CD861BF5B".Sha256()) },

                AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                AllowedScopes = { "scope3" }
            }
        };
    }
}

Duende Software のドキュメントの例では Machine to Machine communication でトークンを取得していますが、そこを Issuing Tokens based on User PasswordsToken Endpoint を参考に id と password を送信してトークンを取得できるように変更しました。AllowedGrantTypes を GrantTypes.ResourceOwnerPassword に設定するのがキモらしいです。

(6) クライアントアプリの作成

IdentityServer に登録済みのユーザーの id と password を送信してトークンを取得し、そのトークンを要求ヘッダに設定して Web API の WeatherForecast アクションメソッドを GET 要求して結果を表示する検証用のアプリを作成します。

Duende Software のドキュメント Creating the client を参考に .NET 6.0 のコンソールアプリとして作成しました。

コードは以下の通りです。NuGet で IdentityModel をインストールしないと動かないので注意してください。

using IdentityModel.Client;
using System.Text.Json;

var client = new HttpClient();

var disco = await client
    .GetDiscoveryDocumentAsync("https://localhost:5001");

if (disco.IsError)
{
    Console.WriteLine(disco.Error);
    return;
}

var tokenResponse2 = await client.RequestPasswordTokenAsync(
    new PasswordTokenRequest
    {
        Address = disco.TokenEndpoint,
        ClientId = "WebApiNet6",
        ClientSecret = "0C86E143-30E0-4FB4-8710-008CD861BF5B",

        Scope = "scope3",

        UserName = "alice",
        Password = "Pass123$"
    });

if (tokenResponse2.IsError)
{
    Console.WriteLine(tokenResponse2.Error);
    return;
}
else
{
    Console.WriteLine(tokenResponse2.Json);
    Console.WriteLine("\n\n");
}

client.Dispose();

var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse2.AccessToken);

var response = await apiClient
              .GetAsync("https://localhost:44300/WeatherForecast");

if (!response.IsSuccessStatusCode)
{
    Console.WriteLine(response.StatusCode);
}
else
{
    var doc = JsonDocument
              .Parse(await response.Content.ReadAsStringAsync())
              .RootElement;
    Console.WriteLine(JsonSerializer
          .Serialize(doc, 
              new JsonSerializerOptions { WriteIndented = true }));
}

apiClient.Dispose();

コンソールアプリの実行結果は以下のようになります。事前に IdentityServer と Web API を動かしておく必要がありますので注意してください。

コンソールアプリの実行結果

一応期待通り動くことは検証できましたが、ホントに上記の設定で良いのかは自信がありません。ひょっとしたら、やってはいけないことをやっているのかもしれませんので、コピペして使うのは避けた方が良いと思います。(笑)

Tags: , ,

Authentication

Duende IdentityServer

by WebSurfer 2022年4月7日 12:38

先の記事「VS2022 .NET 6.0 React のユーザー認証」で IdentityServer を勉強しなくてはと書きましたが、そのための第一歩として Duende Software のサイトの Quickstarts を見ながらサンプルを作ってみました。

Duende IdentityServer

上の画像は Visual Studio 2022 のテンプレートで Duende IdentityServer プロジェクトを作成し、それを Visual Studio から実行してブラウザに表示したものです。

以下に、上の画像の Duende IdentityServer プロジェクトの作り方、ASP.NET Core MVC アプリのプロジェクトを作ってその認証を Duende IdentityServer でサポートする方法を備忘録として書いておきます。

(1) テンプレートのインストール

Overview に従って Visual Studio 用のテンプレートをインストールします。コマンドラインから以下のように dotnet コマンド、

dotnet new --install Duende.IdentityServer.Templates

・・・を実行すると、下の画像のように 6 種類のテンプレートが自動的にインストールされます。

テンプレートをインストール

(2) IdentityServer プロジェクトの作成

Visual Studio 2022 を立ち上げて「新しいプロジェクトの作成」画面を開くと以下のように Duende IdentityServer 関係のメニューが表示されます。

Duende IdentityServer

その中の赤枠で囲った ASP.NET Core Identity をユーザー情報のストアに使うテンプレート選択してプロジェクトを作成してみました。プロジェクトを作成する際何故かフレームワークは選択できず自動的に .NET 6.0 になります。

作成したプロジェクトは Quickstarts の中の Using ASP.NET Core Identity に該当します。(記事が少し古いのか、実際に生成されたものと記事の説明が少々異なるのに注意)

中身は ASP.NET Core の Razor Pages アプリで、Visual Studio から実行すると開発サーバーとして Kestrel が立ち上がってアプリが実行され、この記事の一番上の画像のように結果がブラウザに表示されます。

デフォルトではユーザー情報をストアするためのデータベースは SQLite になります。EF Code First の機能を使ってデータベースを生成します。Migration 関係のファイルは Data/Migrations にありますので、パッケージマネージャーコンソールから update-database を実行すればアプリケーションルート直下に AspIdUsers.db というファイルが生成されます。ただしその時点ではユーザーは登録されてないのでログインはできません。

プロジェクトにはユーザーを登録するためのページは含まれてないのでアプリを操作してユーザー登録することはできません。ユーザー情報をシードするために SeedData.cs がプロジェクトに含まれていますが Visual Studio から実行したのではそれは動きません。

dotnet run /seed を実行すれば alice と bob という名前の 2 人のユーザーがパスワード Pass123$ で登録されるようです (未確認です。dotnet コマンドに自信のない自分は Program.cs のコード中の if (args.Contains("/seed")) を true にして実行してユーザー登録しました)。以降は、そのユーザー情報を使ってログインできるようになります。

以上で IdentityServer 側の準備はできました。ただし、後で、認証をサポートする MVC アプリのホスト名などの情報を Config.cs ファイルの設定に反映する必要があることを覚えておいてください。

(3) ASP.NET Core MVC プロジェクトの作成

次に、Duende Software の Interactive Applications with ASP.NET Core の説明に従って ASP.NET Core MVC アプリのプロジェクトを作成し、上で準備した IdentityServer で認証を行うようにします。

Visual Studio 2022 の「ASP.NET Core Web アプリ (Model-View-Controller)」のテンプレートを使って[フレームワーク(F)]を「.NET 6.0 (長期的なサポート)」とし[認証の種類(A)]を「なし」にして MVC アプリのプロジェクトを作成します。

(4) NuGet パッケージのインストール

MVC プロジェクトに Microsoft.AspNetCore.Authentication.OpenIdConnect を NuGet からインストールします。

Microsoft.AspNetCore.Authentication.OpenIdConnect

(5) Program.cs の編集

MVC プロジェクトの Program.cs を開いて以下の「// 追加」とコメントしてあるコードを追加します。options.ClientId と options.ClientSecret は IdentityServer の Config.cs の Client の設定と合わせる必要があるので注意してください。

// 追加
using System.IdentityModel.Tokens.Jwt;

var builder = WebApplication.CreateBuilder(args);

// 追加
JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();

// 追加
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = "Cookies";
    options.DefaultChallengeScheme = "oidc";
})
    .AddCookie("Cookies")
    .AddOpenIdConnect("oidc", options =>
    {
        // IdentityServer の launchSettings.json の設定と合わせる
        // デフォルトで以下の設定になっているはず
        options.Authority = "https://localhost:5001";

        // IdentityServer の Config.cs の Client の設定と合わせる
        options.ClientId = "interactive";
        options.ClientSecret = "49C1A7E1-0C79-4A89-A3D6-A37998FB86B0";

        options.ResponseType = "code";
        options.SaveTokens = true;

        // 以下を追加すると IdentityServer の SeedData.cs で
        // Claims に追加した name, given_name, family_name
        // 情報も User.Claims に含まれるようになる
        options.Scope.Add("profile");
        options.GetClaimsFromUserInfoEndpoint = true;
    });

// ・・・中略・・・

// 追加
app.UseAuthentication();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

(6) IdentityServer の Config.cs の修正

IdentityServer プロジェクトの Config.cs を開いて RedirectUris, FrontChannelLogoutUri, PostLogoutRedirectUris のホスト名を MVC プロジェクトのものに変更します。

開発環境では MVC プロジェクトの Properties/launchSettings.json に設定されていますのでそれを見るか、実際にアプリを起動してブラウザのアドレスバーを見てください。IIS Express を使うのか Kestrel を使うのかによって変わるので注意してください。

// interactive client using code flow + pkce
new Client
{
    ClientId = "interactive",
    ClientSecrets = { new Secret("49C1A7E1-0C79-4A89-A3D6-A37998FB86B0".Sha256()) },

    AllowedGrantTypes = GrantTypes.Code,

    // 以下のホスト名の部分を修正
    RedirectUris = { "https://localhost:44351/signin-oidc" },
    FrontChannelLogoutUri = "https://localhost:44351/signout-oidc",
    PostLogoutRedirectUris = { "https://localhost:44351/signout-callback-oidc" },

   AllowOfflineAccess = true,
   AllowedScopes = { "openid", "profile", "scope2" }
},

(7) HomeController の修正

MVC プロジェクトの HomeController を開いて Privacy アクションメソッドに [Authorize] 属性を付与します。これにより匿名ユーザーが Privacy にアクセスしようとすると IdentityServer のログインページにリダイレクトされるようになります。

ログアウトできるように Logout アクションメソッドを追加します。その説明は Adding sign-out に書いてあります。(それを読んでも自分には仕組みは分かりませんでしたが、このアクションメソッドを呼び出せばログアウトできるのは確認しました)

using Microsoft.AspNetCore.Mvc;
using MvcNet6.Models;
using System.Diagnostics;

// 追加
using Microsoft.AspNetCore.Authorization;

namespace MvcNet6.Controllers
{
    public class HomeController : Controller
    {
        // ・・・中略・・・        

        // 追加
        [Authorize]
        public IActionResult Privacy()
        {
            return View();
        }

        // 追加
        public IActionResult Logout()
        {
            return SignOut("Cookies", "oidc");
        }

        // ・・・中略・・・
    }
}

(8) Private.cshtml の書き換え

ステップ (7) で [Auhtorize] 属性を付与したアクションメソッド Privacy のビュー Private.cshtml を以下のように書き換えます。これは参考に Claims 情報と IdentityServer のプロパティ情報を表示するためで、こうしなければならないというものではないです。

@using Microsoft.AspNetCore.Authentication

@{
    ViewData["Title"] = "Privacy Policy";
}
<h2>Claims</h2>

<dl>
    @foreach (var claim in User.Claims)
    {
        <dt>@claim.Type</dt>
        <dd>@claim.Value</dd>
    }
</dl>

<h2>Properties</h2>

<dl>
@{
#nullable disable
    foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items)
    {
        <dt>@prop.Key</dt>
        <dd>@prop.Value</dd>
    }
}
</dl>

(9) _LoginPartial.cshtml の追加

ASP.NET Core MVC プロジェクトを認証に個別のアカウントを選んで作成すると、ログインしたときに画面のメニューバーの右端にユーザー名と Logout リンクが表示されますが、それと同等なものを Views/Shared フォルダに _LoginPartial.cshtml として追加します。

<ul class="navbar-nav">
@{
    string? givenName = User.Claims.FirstOrDefault(u => u.Type == "given_name")?.Value;

    if (givenName != null)
    {
        <li class="nav-item">
            <span class="nav-link">Hello @givenName !</span>
        </li>
        <li class="nav-item">
        <a class="nav-link" asp-action="Logout" asp-controller="Home">Logout</a>
        </li>
    }
    else
    {
        <li><span class="nav-link">ログインしていません</span></li>
    }
}
</ul>

これを <partial name="_LoginPartial" /> というコードで Views/Shared/_Layour.cshtml に追加します。場所は <div class="navbar-collapse ... の div 要素の中の一番下です。

以上で完了です。まず Visual Studio で IdentiryServer プロジェクトを実行した後、MVC プロジェクトを実行します。ブラウザに表示された MVC アプリのメニューバーの Pricacy をクリックすると、以下のように IdentityServer のログイン画面にリダイレクトされます。そこで、IdentiryServer プロジェクトで登録したユーザーの alice または bob でログインできます。

ログイン画面

ログインに成功すると Privacy 画面にリダイレクトされ、下の画像の結果が表示されます。

Privacy 画面

メニューバーの右端の Logout リンクをクリックするとログアウトできます。


さて、ここまでやってみて、それで IdentityServer が十分理解できたのか、先の記事「VS2022 .NET 6.0 React のユーザー認証」で問題となった Authority とか Issuer とはどういうものか分かったのかと聞かれると、正直何も分かってないです。

さらなる勉強が必要なようですが、どうも手に負えそうもないような感じがしています。(汗)


【2022/7/12 追記】

認証サーバー (Duende IdentityServer) のドメインは localhost:5001 で、ASP.NET Core MVC のドメインは localhost:44351 と異なっていますが、それで何故 Cookie ベースの ASP.NET Core MVC のユーザー認証がうまくいくのでしょう?

Fiddler を使って調べてみると、認証プロセスの過程で以下の Fiddler のキャプチャ画像の #40 の通り localhost:44351/signin-odic に要求が出て、応答の Set_Cookie で認証クッキーが返ってきています。

認証クッキーも受領

そのあたりの詳しい仕組みは全く分かっていませんが、とにかく ASP.NET Core MVC のドメイン localhost:44351 での要求で認証クッキーが返ってくるので、認証サーバーのドメイン (localhost:5001) が異なっても問題ないということでした。

Tags: , ,

Authentication

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar