vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

ASP.NET Core Identity でログイン・ログアウトしてみたい - AddAuthentication 1

はじめに

前回生成したプロジェクトをベースに、もう少し ASP.NET Core Identity (以下 Identity )について追いかけてみたいと思います。

構造

まず Programming ASP.NET Core によると、下記のような層に分かれて構成されているようです。

f:id:mslGt:20190314223358p:plain

各層の役割を雑にまとめます。

  1. WebApplication ・・・ アプリケーション。 Identity を呼び出す。
  2. UserManager ・・・ 高レベル側のやり取りをする。
  3. UserStore ・・・ 低レベル側のやり取りをする。前回は Entity Framework Core を使った DB アクセスを行っていた。
  4. 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 というのは、チャレンジレスポンス認証を指すようです。

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 にあります。

https://github.com/aspnet/AspNetCore/blob/master/src/Http/Authentication.Core/src/AuthenticationService.cs

コードを見ていくと、 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^)\

長くなってきたのでいったん切ります。

参照