ASP.NET Core 3.1 Web API でトークンベース認証を実装してアクセス制限し、ユーザー認証に ASP.NET Core Identity のユーザー情報を利用する方法を書きます。
.NET Framework Web API の場合は、先の記事「ASP.NET Web API の認証」で書きましたように、Visua Studio のテンプレートを使って認証を「個別のユーザーアカウントカウント」として自動生成すればデフォルトでトークンベースの認証が実装され、認証のためのユーザー情報のストアには ASP.NET Identity が使用されます。
Core Web API では自力での実装が必要になります。テンプレートで認証に「個別のユーザーアカウントカウント」を選択すると「クラウドの既存のユーザーストアに接続する」しか選べません。なので、認証なしの状態から Core 2 からサポートされたという JSON Web Token (JWT) を使った認証を実装することにします。
基本的な方法は Auth0 というサイトのブログの記事「ASP.NET Core 2.0 アプリケーションを JWT でセキュアする」(以下「Auth0 の記事」と書きます)に書いてあるのでそれを見れば済む話なのですが、リンク切れになったりすると困るので要点およびその記事には書いてないことを備忘録として残しておきます。
(1) プロジェクトの作成
元になる ASP.NET Core 3.1 Web API アプリのプロジェクトは Visual Studio Community 2019 のテンプレートで自動生成されたものを使います。以下の画像を見てください。認証は「なし」にしておきます。
テンプレートで自動生成したプロジェクトにはサンプルのコントローラ WeatherForecastController が実装されていて、Visual Studio からプロジェクトを実行([デバッグ(D)]⇒[デバッグなしで開始(H)])すると JSON 文字列が返ってきます。
そのアクションメソッド Get() に JWT ベースの認証を実装します(即ち、トークンが無いとアクセス拒否するようにします)。
(2) NuGet パッケージのインストール
下の画像の赤枠で囲んだ Microsoft.AspNetCore.Authentication.JwtBearer を NuGet からインストールします。青枠で囲んだものは、下に述べたユーザー認証に ASP.NET Core Identity のユーザー情報を利用する場合に必要になります。
(3) JWT 認証スキーマを登録
自動生成された Startup.cs のコードの ConfigureServices メソッドで、AddAuthentication メソッドを使って JWT 認証スキーマを登録します。コードは Auth0 の記事のものをそのままコピペすれば OK です。using 句の追加を忘れないようにしてください。
さらに、認証を有効にするため Configure メソッドに app.UseAuthentication(); を追加します。既存のコードの app.UseAuthorization(); の前にする必要があるので注意してください。
具体的には以下のコードで「JWT ベースの認証を行うため追加」とコメントしたコードを追加します。
// ・・・前略・・・
// JWT ベースの認証を行うため追加
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
//・・・中略・・・
public void ConfigureServices(IServiceCollection services)
{
// JWT ベースの認証を行うため追加
services.AddAuthentication(
JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters =
new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(
Configuration["Jwt:Key"]))
};
});
services.AddControllers();
}
public void Configure(IApplicationBuilder app,
IWebHostEnvironment env)
{
// ・・・中略・・・
// JWT ベースの認証を行うため追加
app.UseAuthentication();
app.UseAuthorization();
//・・・後略・・・
(4) Key と Issuer を appsettings.json に登録
上の (3) コードでは Key と Issuer を appsettings.json ファイルより取得するようにしていますので、以下のように "Jwt" 要素を追加します。
{
・・・中略・・・
"AllowedHosts": "*",
"Jwt": {
"Key": "veryVerySecretKey",
"Issuer": "https://localhost:44330"
}
}
既存の "AllowedHosts": "*" の後にカンマ , を追加するのを忘れないようにしてください。Key はパスワードのようなもので任意の文字列を設定できます(16 文字以上にしないとエラーになるようです)。Issuer はサービスを行う URL にします。
(5) [Authorize] 属性を付与
自動生成された WeatherForecastController コントローラの Get() メソッドに [Authorize] 属性を付与します。using Microsoft.AspNetCore.Authorization; の追加を忘れないようにしてください。
ここまでの設定で JWT トークンベースのアクセス制限の実装は完了しており、トークンなしで WeatherForecastController コントローラの Get() メソッドを要求すると HTTP 401 Unauthorized 応答が返ってくるはずです。
(6) トークンを取得する API を実装
ユーザーの ID とパスワードを送信してトークンを取得する API を実装します。基本的には Auth0 の記事のコントローラ TokenController の通りですが、それを拡張してユーザー情報を既存の ASP.NET Core Identity のデータベースから取得して認証を行うようにしてみました。
コントローラ TokenController のコードは以下の通りです。UserManager<IdentityUser> オブジェクトへの参照を DI によって取得し、それを使って既存の ASP.NET Core Identity から情報を取得してユーザー認証に用いています。
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
// 以下を追加
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using Microsoft.AspNetCore.Identity;
namespace WebApiJwtIdentity.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TokenController : ControllerBase
{
private readonly IConfiguration _config;
private readonly UserManager<IdentityUser> _userManager;
public TokenController(IConfiguration config,
UserManager<IdentityUser> userManager)
{
_config = config;
_userManager = userManager;
}
[AllowAnonymous]
[HttpPost]
public async Task<IActionResult> CreateToken(
LoginModel login)
{
string id = login.Username;
string pw = login.Password;
IActionResult response = Unauthorized();
var user = await _userManager.FindByNameAsync(id);
if (user != null &&
await _userManager.CheckPasswordAsync(user, pw))
{
var tokenString = BuildToken();
response = Ok(new { token = tokenString });
}
return response;
}
private string BuildToken()
{
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var creds = new SigningCredentials(
key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
_config["Jwt:Issuer"],
_config["Jwt:Issuer"],
expires: DateTime.Now.AddMinutes(30),
signingCredentials: creds);
return new JwtSecurityTokenHandler().
WriteToken(token);
}
}
public class LoginModel
{
public string Username { get; set; }
public string Password { get; set; }
}
}
上記のコード以外にも以下の追加が必要です。
(a) 上の (2) の画像で青枠で囲んだ NuGet パッケージのインストール。
(b) IdentityDbContext を継承した ApplicationDbContext クラスを追加。Data フォルダを作ってそれにクラスファイルとして実装します。
using System;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace WebApiJwtIdentity.Data
{
public class ApplicationDbContext : IdentityDbContext
{
public ApplicationDbContext(
DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
}
(c) appsettings.json に ASP.NET Core Identity が使う既存の SQL Server DB への接続文字列を追加。
(d) Startup.cs に以下を追加。これらは上の「(3) JWT 認証スキーマを登録」に書いた JWT ベースの認証を行うため追加したコード services.AddAuthentication(...); より前に持ってくる必要があるので注意。そうしないとトークンによる認証が通らない。
// 追加
using Microsoft.AspNetCore.Identity;
using WebApiJwtIdentity.Data;
using Microsoft.EntityFrameworkCore;
public void ConfigureServices(IServiceCollection services)
{
// 追加
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString(
"MvcCoreIdentityContextConnection")));
// 追加
services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
(7) 検証用 Home/Index ページを追加
以下は必須ではないですが、検証用の Home/Index ページを追加し、そこから jQuery ajax を使ってトークンの取得と認証が期待通りとなるかを確認してみます。
View のコードは以下のようになります。下のコードの Username と Password には "***" ではなくて有効な文字列を設定してください。このページを使って確認した結果が一番上の画像です。
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
<script src="~/Scripts/jquery.js"></script>
<script type="text/javascript">
//<![CDATA[
var tokenKey = 'accessToken';
function getToken() {
var obj = { Username : "***", Password : "***" };
var jsonString = JSON.stringify(obj);
$.ajax({
type: "POST",
url: "/api/token",
data: jsonString,
contentType: "application/json; charset=utf-8",
success: function (data, textStatus, jqXHR) {
sessionStorage.setItem(tokenKey, data.token);
},
error: function (jqXHR, textStatus, errorThrown) {
$('#output').empty();
$('#output').text('textStatus: ' + textStatus +
', errorThrown: ' + errorThrown);
}
});
}
function weatherForecast() {
var token = sessionStorage.getItem(tokenKey);
var headers = {};
if (token) {
headers.Authorization = 'Bearer ' + token;
}
$.ajax({
type: "GET",
url: "/WeatherForecast",
headers: headers,
cache: false,
success: function (data, textStatus, jqXHR) {
$('#output').empty();
$.each(data, function (key, val) {
var day = new Date(val.date);
var dateString = day.getFullYear() + "年" +
(day.getMonth() + 1) + "月" +
day.getDate() + "日";
$('#output').append(
'<p>' + dateString + ' / ' +
val.temperatureC + ' / ' +
val.temperatureF + ' / ' +
val.summary + '</p >');
});
},
error: function (jqXHR, textStatus, errorThrown) {
$('#output').empty();
$('#output').text('textStatus: ' + textStatus +
', errorThrown: ' + errorThrown);
}
});
}
//]]>
</script>
</head >
<body>
<h3>Web API から jQuery ajax を使ってデータの取得</h3>
<input type="button" id="Button1"
value="WeatherForecast" onclick="weatherForecast();" />
<input type="button" id="Button2"
value="Get Token" onclick="getToken();" />
<hr />
<p>結果の表示:</p>
<div id="output"></div>
</body>
</html >
なお、元々のプロジェクトの設定が Web API 用ですので、そのままでは MVC 用の Controller と View は動きませんので注意してください。以下の設定が必要になります。
(a) Startup.cs で MVC 用のサービスの追加、静的ファイルの利用を可能にすること、ルーティングのためのマップ設定。
(b) launchSettings.json で "launchUrl" の "weatherforecast" を "home/index" に変更。
(c) jQuery を利用するので wwwroot/Script/jQuery.js を追加。