ASP.NET Core Identity でログイン・ログアウトしてみたい - AddAuthentication 1
はじめに
前回生成したプロジェクトをベースに、もう少し ASP.NET Core Identity (以下 Identity )について追いかけてみたいと思います。
構造
まず Programming ASP.NET Core によると、下記のような層に分かれて構成されているようです。
各層の役割を雑にまとめます。
- WebApplication ・・・ アプリケーション。 Identity を呼び出す。
- UserManager ・・・ 高レベル側のやり取りをする。
- UserStore ・・・ 低レベル側のやり取りをする。前回は Entity Framework Core を使った DB アクセスを行っていた。
- PhysicalStore ・・・ 実際にデータを保存する。今回は PostgreSQL。
前回 1., 3. のクラスは自作し(ほぼコピペですが)、 4.も用意しました。
ということで全く触れていないのは 2.のみですが、それぞれどのように動いているのかもう少し見ていきます。
IdentityBuilder
どこからたどっていくか。。。というところなのですが、とりあえず User や UserStore などを登録していた、 Startup > ConfigureServices から手を付けてみることにします。
なお NuGet などで追加インストールをしなくても利用できる ASP.NET Core Identity ですが、プロジェクトは分割されているようです。
こちらのコードも参照しながらだいたいどのようなことをしているのか、を追いかけてみたいと思います。
Startup.cs
~省略~ public void ConfigureServices(IServiceCollection services) { ~省略~ services.AddIdentity< ApplicationUser, IdentityRole>() .AddUserStore< ApplicationUserStore>() .AddEntityFrameworkStores< LoginLogoutSampleContext>() .AddDefaultTokenProviders(); ~省略~ } ~省略~
User 、 Role をどこかに追加している AddIdentity ですが、このメソッドは Identity > src > Identity > IdentityServiceCollectionExtensions.cs に定義されています。
IdentityServiceCollectionExtensions.cs
public static IdentityBuilder AddIdentity< TUser, TRole>( this IServiceCollection services) where TUser : class where TRole : class => services.AddIdentity< TUser, TRole>(setupAction: null);
ここでは IServiceCollection (実装クラスは Microsoft.Extensions.DependencyInjection.ServiceCollection )の拡張メソッドとして、 AddIdentity を追加しています。
参照
IdentityServiceCollectionExtensions.cs
~省略~ public static IdentityBuilder AddIdentity< TUser, TRole>( this IServiceCollection services, Action< IdentityOptions> setupAction) where TUser : class where TRole : class { // Services used by identity services.AddAuthentication(options => { options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme; options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme; options.DefaultSignInScheme = IdentityConstants.ExternalScheme; }) .AddCookie(IdentityConstants.ApplicationScheme, o => { o.LoginPath = new PathString("/Account/Login"); o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync }; }) .AddCookie(IdentityConstants.ExternalScheme, o => { o.Cookie.Name = IdentityConstants.ExternalScheme; o.ExpireTimeSpan = TimeSpan.FromMinutes(5); }) .AddCookie(IdentityConstants.TwoFactorRememberMeScheme, o => { o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme; o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = SecurityStampValidator.ValidateAsync< ITwoFactorSecurityStampValidator> }; }) .AddCookie(IdentityConstants.TwoFactorUserIdScheme, o => { o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme; o.ExpireTimeSpan = TimeSpan.FromMinutes(5); }); // Hosting doesn't add IHttpContextAccessor by default services.AddHttpContextAccessor(); // Identity services services.TryAddScoped< IUserValidator< TUser>, UserValidator< TUser>>(); services.TryAddScoped< IPasswordValidator< TUser>, PasswordValidator< TUser>>(); services.TryAddScoped< IPasswordHasher< TUser>, PasswordHasher< TUser>>(); services.TryAddScoped< ILookupNormalizer, UpperInvariantLookupNormalizer>(); services.TryAddScoped< IRoleValidator< TRole>, RoleValidator< TRole>>(); // No interface for the error describer so we can add errors without rev'ing the interface services.TryAddScoped< IdentityErrorDescriber>(); services.TryAddScoped< ISecurityStampValidator, SecurityStampValidator< TUser>>(); services.TryAddScoped< ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator< TUser>>(); services.TryAddScoped< IUserClaimsPrincipalFactory< TUser>, UserClaimsPrincipalFactory< TUser, TRole>>(); services.TryAddScoped< UserManager< TUser>>(); services.TryAddScoped< SignInManager< TUser>>(); services.TryAddScoped< RoleManager< TRole>>(); if (setupAction != null) { services.Configure(setupAction); } return new IdentityBuilder(typeof(TUser), typeof(TRole), services); } ~省略~
Oh...めっさ追加されてる。。。
大きく AddAuthentication による認証 + Cookie 関連の設定と、 TryAddScoped として 前回登場した PasswordHasher や SignInManager などの追加が行われているようです。
また、下記のように Identity を使わず AddAuthentication を直接使うこともできるようです。
見ただけでお腹いっぱいになりそうな気が早くもしていますが、力尽きるまで追いかけてみますよ。
services.AddAuthentication
まずは認証の部分から。
IdentityServiceCollectionExtensions.cs
~省略~ services.AddAuthentication(options => { options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme; options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme; options.DefaultSignInScheme = IdentityConstants.ExternalScheme; }) ~省略~
ここでは AuthenticationOptions として、 HTTP 認証方法( AuthenticateScheme )を指定しているようです。
引数の Action< AuthenticationOptions> で渡している DefaultXXXXScheme ですが、各値は Identity > src > Identity > IdentityConstants.cs に定義されており、下記のような内容でした。
- Identity.Application
- Identity.External
DefaultAuthenticateScheme というのは Basic 認証(ユーザー名とパスワードによる認証)のことでしょうか。
次の DefaultChallengeScheme というのは、チャレンジレスポンス認証を指すようです。
サーバー側から nonce と呼ばれるランダムな値を送信し、クライアントでユーザー名、パスワードなどをハッシュ化して返し、サーバー側で nonce の値を元に受け取った値を復元 -> 認証する方式。
Challenge-response authentication (チャレンジレスポンス認証) - MDN Web Docs 用語集: ウェブ関連用語の定義 - MDN
- Understanding HTTP Authentication - Microsoft Docs
DefaultSignInScheme として External (外部の) スキーマを指定しています。何となく Twitter など外部サービスのアカウントで認証する、ということを想像してしまいますが、これとは違うのでしょうか。
AuthenticationBuilder
さてこの AddAuthentication ですが、 AspNetCore > src > Security > Authentication > Core > src > AuthenticationServiceCollectionExtensions.cs が呼ばれており、 AuthenticationBuilder が返されます。
AddAuthentication では先ほどの DefaultXXXXScheme の値をオプションとして services.Configure< AuthenticationOptions> に設定するほか、オーバーロードされた下記を呼び出します。
AuthenticationServiceCollectionExtensions.cs
~省略~ public static AuthenticationBuilder AddAuthentication(this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(services)); } services.AddAuthenticationCore(); services.AddDataProtection(); services.AddWebEncoders(); services.TryAddSingleton< ISystemClock, SystemClock>(); return new AuthenticationBuilder(services); } ~省略~
ここでは 4 つのものが追加されているようです。
まずは services.AddAuthenticationCore() を見てみます。
services.AddAuthenticationCore()
services.AddAuthenticationCore() は、 AspNetCore > src > Http > Authentication.Core > src > AuthenticationCoreServiceCollectionExtensions.cs で定義されています。
AuthenticationCoreServiceCollectionExtensions.cs
~省略~ public static IServiceCollection AddAuthenticationCore(this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(services)); } services.TryAddScoped< IAuthenticationService, AuthenticationService>(); services.TryAddSingleton< IClaimsTransformation, NoopClaimsTransformation>(); // Can be replaced with scoped ones that use DbContext services.TryAddScoped< IAuthenticationHandlerProvider, AuthenticationHandlerProvider>(); services.TryAddSingleton< IAuthenticationSchemeProvider, AuthenticationSchemeProvider>(); return services; } ~省略~
また 4 つ追加されています。
まず初めの AuthenticationService ですが、これも AspNetCore > src > Http > Authentication.Core > src にあります。
コードを見ていくと、 AuthenticateAsync や SignInAsync など、前回 SignInManager を使ってログイン・ログアウトしたときのベースらしきメソッドが見つかります。
試しに SignInAsync を見てみることにします。
AuthenticationService.cs
~省略~ public virtual async Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } if (scheme == null) { var defaultScheme = await Schemes.GetDefaultSignInSchemeAsync(); scheme = defaultScheme?.Name; if (scheme == null) { throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignInScheme found."); } } var handler = await Handlers.GetHandlerAsync(context, scheme); if (handler == null) { throw await CreateMissingSignInHandlerException(scheme); } var signInHandler = handler as IAuthenticationSignInHandler; if (signInHandler == null) { throw await CreateMismatchedSignInHandlerException(scheme, handler); } await signInHandler.SignInAsync(principal, properties); } ~省略~
引数として渡された scheme ( Identity.Application ) をもとに、 Handlers から IAuthenticationSignInHandler を取得し、 SignInAsync() を呼んでいる、と。
で、この Handlers というのが何か?というと AddAuthenticationCore() で 3 つ目に渡していた AuthenticationHandlerProvider です。
( AspNetCore > src > Http > Authentication.Core > src > AuthenticationHandlerProvider.cs )
AuthenticationService へは DI で渡されます。
今回 signInHandler として渡されているのは、 CookieAuthenticationHandler でした( BreakPoint を置いて確認)。
CookieAuthenticationHandler
この CookieAuthenticationHandler.cs は、 AspNetCore > src > Security > Authentication > Cookies > src にあります。
が、 SignInAsync は見つからない。。。(´・ω・`)
AspNetCore > src > Security > Authentication > Core > src > SignInAuthenticationHandler.cs
と
AspNetCore > src > Http > Authentication.Abstractions > src > AuthenticationHttpContextExtensions.cs
を辿っていくと、下記のクラスにたどり着きました。
AuthenticationService.cs
/(^o^)\
長くなってきたのでいったん切ります。