.NET Framework 4.8 で動いている Web API を .NET Core 3.1 で動いている IdentityServer4 で保護する

はじめに

大人の事情によって .NET Framework 4.8 で動かさざるをえない ASP.NET Core 2.2 製 Web API を、OAuth2.0 か OpenID Connect に対応させる必要があり、IdentityServer4 を使う予定でいた。

その IdentityServer4 は 5/1 時点で v3.1.3 が最新。もうすぐ v4.0.0 が出そうですらある。そして、IdentityServer4 は v3.0.0 から .NET Core のみサポートになっていた。v3.1.3 は .NET Core 3.1 前提。

Web API と IdentityServer4 ホストで、ASP.NET Core のバージョンを揃えたい場合は、.NET Standard 2.0 をサポートしていた v2.5.4 まで IdentityServer4 のバージョンを下げることになるが、さすがに古すぎる。

そもそも、IdentityServer4 が実装している OAuth2.0 や OpenID Connect は、HTTP と同じように仕様がちゃんと決まっているわけで、仕様通りに実装されてさえいれば ASP.NET Core のバージョンが違っても大丈夫なはず。というわけで試してみた。

IdentityServer4 ホストを .NET Core 3.1 と ASP.NET Core 3.1 で作成

IdentityServer4 は v4.0.0 がもうそろそろ出そうだけど、プレビューを除いて一番新しいリリースである v3.1.3 を使う。

www.nuget.org

コードはほぼ IdentityServer4 のドキュメントのまんま。

using System.Collections.Generic;
using IdentityServer4.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace IdentityServer4Sample
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>()
                        .UseUrls("http://localhost:5000");
                });
    }

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
                .AddInMemoryApiResources(Config.Apis)
                .AddInMemoryClients(Config.Clients)
               .AddDeveloperSigningCredential();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseIdentityServer();
        }
    }

    public static class Config
    {
        public static IEnumerable<ApiResource> Apis =>
            new List<ApiResource>
            {
                new ApiResource("api1", "My API")
            };

        public static IEnumerable<Client> Clients =>
            new List<Client>
            {
                new Client
                {
                    ClientId = "client",

                    AllowedGrantTypes = GrantTypes.ClientCredentials,

                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },

                    AllowedScopes = { "api1" }
                }
            };
    }
}

Web API .NET Framework 4.8 と ASP.NET Core 2.2 で作成

アクセストークンの検証には、IdentityServer4.AccessTokenValidation の v2.7.0 を使う。.NET Standard 2.0 をサポートしているのはそこまでなので。

www.nuget.org

Web API の実装は、ほぼ IdentityServer4 v2 系のドキュメントのまんま。

using System.Linq;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace SampleApi
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseUrls("http://localhost:5001")
                .UseStartup<Startup>();
    }

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

            services.AddAuthentication("Bearer")
                .AddIdentityServerAuthentication(options =>
                {
                    options.Authority = "http://localhost:5000";
                    options.RequireHttpsMetadata = false;

                    options.ApiName = "api1";
                });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseAuthentication();

            app.UseMvc();
        }
    }

    [Route("identity")]
    [Authorize]
    public class IdentityController : ControllerBase
    {
        [HttpGet]
        public IActionResult Get()
        {
            return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
        }
    }
}

Web API を呼び出すクライアントを .NET Core 3.1 で作成

アクセストークンの取得に使うライブラリ IdentityModel は、.NET Standard 2.0 をサポートしているので .NET Framework 4.8 でも良かったけど、今回の実験においてクライアントは特に重要じゃ無いので .NET Core 3.1 にしておく。

using System;
using System.Net.Http;
using System.Threading.Tasks;
using IdentityModel.Client;
using Newtonsoft.Json.Linq;

namespace SampleClient
{
    class Program
    {
        static async Task Main(string[] args)
        {
            // メタデータからエンドポイントの情報を取得
            var client = new HttpClient();
            var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000");
            if (disco.IsError)
            {
                Console.WriteLine(disco.Error);
                goto END;
            }

            // トークンを取得
            var tokenResponse = await client.RequestClientCredentialsTokenAsync(
                new ClientCredentialsTokenRequest
                {
                    Address = disco.TokenEndpoint,
                    ClientId = "client",
                    ClientSecret = "secret",
                    Scope = "api1"
                });

            if (tokenResponse.IsError)
            {
                Console.WriteLine(tokenResponse.Error);
                goto END;
            }

            Console.WriteLine(tokenResponse.Json);

            // Web API を呼び出す
            var apiClient = new HttpClient();
            apiClient.SetBearerToken(tokenResponse.AccessToken);

            var response = await apiClient.GetAsync("http://localhost:5001/identity");
            if (!response.IsSuccessStatusCode)
            {
                Console.WriteLine(response.StatusCode);
            }
            else
            {
                var content = await response.Content.ReadAsStringAsync();
                Console.WriteLine(JArray.Parse(content));
            }

        END:
            Console.ReadLine();
        }
    }
}

実行結果

.NET Core 3.1 で動いている IdentityServer4 から取得したアクセストークンを使って、.NET Framework 4.8 で動いている Web API を呼び出すことに成功した。

f:id:griefworker:20200424110717p:plain

まとめ

.NET Framework 4.8 で動いている Web API を、.NET Core 3.1 で動いている IdentityServer4 で問題なく保護できた。 OpenID Connect および OAuth 2.0 はちゃんと仕様が存在していて、IdentityServer4 はその仕様を実装しているだけ。 組み合わせるライブラリのバージョンが違っていても動くだろうとは思っていたので、今回の実験で確証が持てて一安心だ。