ASP.NET Core で認証を使っていて、標準のクレーム以外を User.Identity に持たせたくなったので実現する方法を調べました。ASP.NET 時代では Global.asax でイベントハンドラーを追加して Context.User を入れ替えて実現してましたが、ASP.NET Core にはそんなものはありません。
良い感じに認証クッキーからクレームを復元するタイミングで、何らかの処理を挟めないか調べてみると IClaimsTransformation というインターフェースが見つかりました。
このインターフェースは TransformAsync というメソッドのみ持っているシンプルなものです。
もはや説明は不要な気がしますが、このメソッドの実装でいい感じに ClaimsPrincipal に対してクレームの追加を行った後に返せば、コントローラやビューなどで追加したクレームを参照できるようになります。
デフォルトでは NoopClaimsTransformation が組み込まれていて、実装はそのまま渡された ClaimsPrincipal を返しているだけです。とりあえず独自の変換を実装する前に動きを確認しておきます。
public class MyClaimsTransformation : IClaimsTransformation { public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) { return Task.FromResult(principal); } }
適当に渡された ClaimsPrincipal をそのまま返す実装を用意しました。
実際に ASP.NET Core の認証処理に組むこむためには DI を使って、実装したクラスを登録する必要があるのはこれまで通りです。ASP.NET Core はいろんな処理を DI で差し替えることが出来ますが、柔軟すぎて逆にカスタマイズが難しい部分もあります。
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true) .AddEntityFrameworkStores<ApplicationDbContext>(); // IClaimsTransformation を継承したクラスを DI に追加 services.AddScoped<IClaimsTransformation, MyClaimsTransformation>(); services.AddControllersWithViews(); services.AddRazorPages(); } }
DI に追加してしまえば設定は完了なので、動かしてみて変換処理が呼ばれるかを確認しておきました。
未ログイン状態の挙動が気になりましたが、ログインしていないときには変換処理は呼ばれませんでした。なので TransformAsync はログインされている前提のコードで良さそうです。

変換メソッドは Task を返すようになっているのと、DI を使って他のインスタンスを取ることができるので、内部で DB へのアクセス処理なども簡単に書けて便利です。
サンプルとして登録されているメールアドレスと Gravatar の URL をクレームに追加する変換処理を実装してみました。Gravatar の URL はメールアドレスの MD5 を計算するだけなので簡単です。
今回実装したクレーム変換の処理は以下のようになります。コンストラクタで ASP.NET Core Identity 向けの DbContext 実装を受け取って、TransformAsync で DB にクエリを投げてメールアドレスを取得するようにしています。特に難しいことはしていないです。
DI と async / await が使えるので、無理のない自然なコードで書けて最高です。
public class MyClaimsTransformation : IClaimsTransformation { public MyClaimsTransformation(ApplicationDbContext applicationDbContext) { _applicationDbContext = applicationDbContext; } private readonly ApplicationDbContext _applicationDbContext; public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) { var identity = (ClaimsIdentity)principal.Identity; var user = await _applicationDbContext.Users.FindAsync(principal.FindFirstValue(ClaimTypes.NameIdentifier)); identity.AddClaim(new Claim(ClaimTypes.Email, user.Email)); identity.AddClaim(new Claim("AvatarImage", GenerateGravatarUrl(user.Email))); return principal; } private string GenerateGravatarUrl(string email) { using var md5 = MD5.Create(); var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(email.ToLower())); var hashString = new StringBuilder(); for (int i = 0; i < hash.Length; i++) { hashString.Append(hash[i].ToString("x2")); } return $"https://www.gravatar.com/avatar/{hashString}"; } }
ASP.NET Core Identity には DB にクレームを永続化する機能もありますが、今回の IClaimsTransformation を使った方法ではデータソースに任意のものを利用出来るので簡単です。微妙に Identity 周りはブラックボックスになってるので、クレームの追加も面倒な感じです。
適当なページにログインしているユーザーのクレーム一覧を出すようにして確認してみました。

ちゃんと追加したメールアドレスと Gravatar の URL がクレームに含まれていることが確認できますね。クレームに追加することで、いろんな部分で HttpContext.User 経由して値が利用できるようになります。
以下のようにビューに Gravatar のアイコンを表示する定義も簡単に書けます。
@if (SignInManager.IsSignedIn(User))
{
<li class="nav-item">
<img src="@(User.FindFirstValue("AvatarImage"))" height="42" />
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity.Name!</a>
</li>
<li class="nav-item">
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Action("Index", "Home", new { area = "" })">
<button type="submit" class="nav-link btn btn-link text-dark">Logout</button>
</form>
</li>
}
実行してみると、ユーザー名の隣に Gravatar で登録したアイコンが表示されました。

実際にアプリケーションを書いていると、標準のクレームでは足りなくて都度 ViewBag に入れたり、JavaScript で取ってきたりと苦労が色々とありましたが、シンプルにクレームに入れてしまえば ASP.NET Core 全体で使えるようになるので非常に便利です。
クレームを ClaimsPrincipal から取るのが少し面倒ですが、拡張メソッドを用意してあげれば間違えることもありません。これで実際にかなり見通しの良いコードになりました。