WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

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

by WebSurfer 8. April 2022 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

About this blog

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

Calendar

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

View posts in large calendar