この記事は Microsoft Student Partners Japan アドベントカレンダーの12日目の記事です。


はじめに

ASP.NET Core のプロジェクトをアカウント認証付きで作成すると、Cookie認証によってWebページでの認証が行われるようになります。

Ajax通信なども、このCookieを勝手に使ってくれるので特に気にする必要はないのですが、モバイルアプリから接続するようなとき、APIを叩くための認証ではCookie認証よりToken認証の方が適しています。

今回は、ASP.NET Core 2系での Bearer Token 認証の方法について書いていきます。

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

今回は、OpenIddict というライブラリを使用して実装をしていきます。

まず、このライブラリは Nuget.org 上ではなく、myget.org の方でダウンロードが可能になっているので、パッケージソースを追加します。

パッケージソースの追加

環境によって方法は違いますが、WindowsのVisualStudioでは、NuGet パッケージマネージャーの右上にある歯車アイコンを選択し、パッケージソースの一覧画面に移動します。

+アイコンで新しいソースを作成し、以下の内容に書き換えます

1
2
名前:aspnet-contrib
ソース:https://www.myget.org/F/aspnet-contrib/api/v3/index.json

パッケージの追加

ソースを追加したら、以下の4つのパッケージを追加します

1
2
3
4
OpenIddict
OpenIddict.EntityFrameworkCore
OpenIddict.Mvc
AspNet.Security.OAuth.Validation

コードの追加

次は実際にコードを書いていきます。

コアな部分

Startup.csConfigureServices メソッド内部を以下のように変えていきます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public void ConfigureServices(IServiceCollection services)
{
    //services.AddDbContext<ApplicationDbContext>(options =>
    //    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
    services.AddDbContext<ApplicationDbContext>(options =>
    {
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
        options.UseOpenIddict(); // ここを呼ぶようにする
    });

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddAuthentication() // この3行を追加
        .AddCookie()
        .AddOAuthValidation();

    services.AddTransient<IEmailSender, EmailSender>();

    services.AddMvc();

    // ここから下を追加
    services.AddOpenIddict(options =>
    {
        options.AddEntityFrameworkCoreStores<ApplicationDbContext>();
        options.AddMvcBinders();
        options.EnableAuthorizationEndpoint("/connect/authorize");
        options.EnableTokenEndpoint("/connect/token");
        options.AllowPasswordFlow();
        options.DisableHttpsRequirement();
    });

    // このセクションも追加
    services.Configure<IdentityOptions>(options =>
    {
        options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
        options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
        options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
    });
}

これでエンドポイントの作成まで完了です。

クライアント登録

次に、Bearer認証に対応するアプリケーションの登録を行います。

初期データの投入と同じタイミングで行うので、 Program.csMain メソッドに記述していきます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public static void Main(string[] args)
{
    var host = BuildWebHost(args);

    using (var scope = host.Services.CreateScope())
    {
        var services = scope.ServiceProvider;
        try
        {
            Initialize(services).Wait();
        }
        catch (Exception ex)
        {
            var logger = services.GetRequiredService<ILogger<Program>>();
            logger.LogError(ex, "An error occurred seeding the DB.");
        }
    }

    host.Run();
}

private static async Task Initialize(IServiceProvider services)
{
    var context = services.GetRequiredService<ApplicationDbContext>();
    await context.Database.EnsureCreatedAsync();

    var manager = services.GetRequiredService<OpenIddictApplicationManager<OpenIddictApplication>>();

    if (await manager.FindByClientIdAsync("sample-client", CancellationToken.None) == null)
    {
        var descriptor = new OpenIddictApplicationDescriptor
        {
            ClientId = "sample-client",
            ClientSecret = "ed9b9fd101a9392e8ae91d4b0cf04c65b6f6d326a4bff9cb24bfd7a0a81d64fa",
            DisplayName = "Sample Client"
        };

        await manager.CreateAsync(descriptor, CancellationToken.None);
    }
}

ClientIdClientSecret らへんは適当に入れ替えて下さい。

コントローラーを追加する

ここで追加するのは、Bearerトークンで認証されているアクセスのみアクセス可能にするコントローラーです。

CookieでもBearerでも両方可能にする方法はまだ見つかっていません。。。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[Produces("application/json")]
[Route("api/sample")]
[Authorize(AuthenticationSchemes = OAuthValidationDefaults.AuthenticationScheme)]
public class SampleController : Controller
{
    public IActionResult Get()
    {
        return Ok("success");
    }
}

こんな感じに、Authorizeアトリビュートにオプションを渡すだけです。

データベースの更新

最後に、OpenIddictで追加されるモデルを使用できるようにマイグレーションをかけます。

1
2
dotnet ef migrations add AddOpenIddict
dotnet ef database update

クライアント側から使う

残りは普通のBearer認証と同じでできます。

自分はJavaから使用しましたが、こんな感じでフルスクラッチでかけます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public AccessToken tryLogin(String email, String password) {
    System.out.println("Start authenticate");
    byte[] body = Utils.convertToFormUrlEncoded(new String[][]{
            {"grant_type", "password"},
            {"username", email},
            {"password", password},
            {"client_id", config.clientId},
            {"client_secret", config.clientSecret}
    }).getBytes(StandardCharsets.UTF_8);
    try {
        URL url = new URL(config.hostname + "/connect/token");
        HttpURLConnection connection = null;
        try {
            connection = (HttpURLConnection) url.openConnection();
            connection.setDoOutput(true);
            connection.setRequestMethod("POST");
            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
            connection.setRequestProperty("charset", "utf-8");
            connection.setUseCaches(false);
            try (DataOutputStream dos = new DataOutputStream(connection.getOutputStream())) {
                dos.write(body);
            }
            connection.connect();
            if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
                String jsonRaw = Utils.readAll(connection.getInputStream());
                Json json = Json.read(jsonRaw);
                return new AccessToken(
                        json.at("resource").asString(),
                        json.at("token_type").asString(),
                        json.at("access_token").asString(),
                        json.at("expires_in").asInteger());
            } else {
                throw new RuntimeException("Response: " + connection.getResponseCode());
            }
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

サンプル

一通り実装したサンプルをこちらに置いておきます。