この記事は 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.cs
の ConfigureServices
メソッド内部を以下のように変えていきます。
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.cs
の Main
メソッドに記述していきます。
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);
}
}
|
ClientId
や ClientSecret
らへんは適当に入れ替えて下さい。
コントローラーを追加する
ここで追加するのは、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);
}
}
|
サンプル
一通り実装したサンプルをこちらに置いておきます。