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
から取るのが少し面倒ですが、拡張メソッドを用意してあげれば間違えることもありません。これで実際にかなり見通しの良いコードになりました。