ASP.NET Core Identity をテンプレートからカスタマイズ

こんにちは。

クラウド事業本部コンサルティング&テクノロジー部の茨木です。

 

今回は、ASP.NET Core Identity です。

Visual Studio がテンプレート実装をしてくれますが、そのまま全て動かせる程に構成済み状態となっています。カスタマイズしようにも構成はDIで隠蔽されていて、初見ではどこから手を付けていいのか迷いますね。しかも SQL Server を前提に構成された上に、EntityFramework まで組み付けられてしまいますが、こんなの邪魔ですよね。ということで、テンプレートに手を加え、ASP.NET Core Identity に則りつつ、独自のデータストアで認証を実装してみたいと思います。

 

この記事はネクストスケープ クラウド事業本部 Advent Calendar 2017 の9日目となります。




プロジェクト作成



プロジェクトテンプレートからプロジェクトを作成します。ここで認証を設定しておくと、ASP.NET Core Identity が組み込まれたテンプレートが生成されます。



カスタマイズ



Startup.cs



public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    // Add application services.
    services.AddTransient<IEmailSender, EmailSender>();

    services.AddMvc();
}

黄色の部分が要りません。消しちゃいましょう。

この状態で起動すると必要なクラスが欠損しているためDIの依存関係解決ができません。欠損している実装によって、概ね以下のパターンでエラーとなって失敗します。


IUserStore が解決できない
An unhandled exception occurred while processing the request.
InvalidOperationException: Unable to resolve service for type 'Microsoft.AspNetCore.Identity.IUserStore`1[WebApplication1.Models.ApplicationUser]' while attempting to activate 'Microsoft.AspNetCore.Identity.AspNetUserManager`1[WebApplication1.Models.ApplicationUser]'.
Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type serviceType, Type implementationType, ISet<Type> callSiteChain, ParameterInfo[] parameters, bool throwIfCallSiteNotFound)


IRoleStore が解決できない
An unhandled exception occurred while processing the request.
InvalidOperationException: Unable to resolve service for type 'Microsoft.AspNetCore.Identity.IRoleStore`1[Microsoft.AspNetCore.Identity.IdentityRole]' while attempting to activate 'Microsoft.AspNetCore.Identity.AspNetRoleManager`1[Microsoft.AspNetCore.Identity.IdentityRole]'.
Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type serviceType, Type implementationType, ISet&lt;Type&gt; callSiteChain, ParameterInfo[] parameters, bool throwIfCallSiteNotFound)


IUserStore の実装クラスが IUserPasswordStore を実装していない
An unhandled exception occurred while processing the request.
NotSupportedException: Store does not implement IUserPasswordStore&lt;TUser&gt;.
Microsoft.AspNetCore.Identity.UserManager.GetPasswordStore()


ここがテンプレート実装から削ってしまった部分なので、IUserStore, IUserPasswordStore, IRoleStore を実装して組み込む必要があります。



MyUserStore : IUserStore, IUserPasswordStore



独自のユーザー管理ストレージとして、ここでは簡易的にインメモリでユーザー管理をします。

class MyUserStore : IUserStore<ApplicationUser>, IUserPasswordStore<ApplicationUser>
{
    private static readonly List<ApplicationUser> InMemoryStore = new List<ApplicationUser>();

    public void Dispose() { }

    public async Task<string> GetUserIdAsync(ApplicationUser user, CancellationToken cancellationToken)
        => user.Id;

    public async Task<string> GetUserNameAsync(ApplicationUser user, CancellationToken cancellationToken)
        => user.UserName;

    public async Task SetUserNameAsync(ApplicationUser user, string userName, CancellationToken cancellationToken)
        => user.UserName = userName;

    public async Task<string> GetNormalizedUserNameAsync(ApplicationUser user, CancellationToken cancellationToken)
        => user.NormalizedUserName;

    public async Task SetNormalizedUserNameAsync(ApplicationUser user, string normalizedName, CancellationToken cancellationToken)
        => user.NormalizedUserName = normalizedName;
    
    public async Task<IdentityResult> CreateAsync(ApplicationUser user, CancellationToken cancellationToken)
    {
        InMemoryStore.Add(user);
        return IdentityResult.Success;
    }

    public async Task<IdentityResult> UpdateAsync(ApplicationUser user, CancellationToken cancellationToken)
    {
        var index = InMemoryStore.FindIndex(a => a.Id == user.Id);
        InMemoryStore[index] = user;
        return IdentityResult.Success;
    }

    public async Task<IdentityResult> DeleteAsync(ApplicationUser user, CancellationToken cancellationToken)
    {
        var index = InMemoryStore.FindIndex(a => a.Id == user.Id);
        InMemoryStore.RemoveAt(index);
        return IdentityResult.Success;
    }

    public async Task<ApplicationUser> FindByIdAsync(string userId, CancellationToken cancellationToken) =>
        InMemoryStore.FirstOrDefault(a => a.Id == userId);

    public async Task<ApplicationUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) =>
        InMemoryStore.FirstOrDefault(a => a.NormalizedUserName == normalizedUserName);

    public async Task SetPasswordHashAsync(ApplicationUser user, string passwordHash, CancellationToken cancellationToken)
        => user.PasswordHash = passwordHash;

    public async Task<string> GetPasswordHashAsync(ApplicationUser user, CancellationToken cancellationToken)
        => user.PasswordHash;

    public async Task<bool> HasPasswordAsync(ApplicationUser user, CancellationToken cancellationToken)
        => true;
}


MyRoleStore : IRoleStore



今回Roleは使いませんが、インスタンスはDIできる必要があるのでクラス定義だけ用意します。

class MyRoleStore : IRoleStore<IdentityRole>
{
    public void Dispose() { }

    public Task<IdentityResult> CreateAsync(IdentityRole role, CancellationToken cancellationToken)
        => throw new NotImplementedException();

    public Task<IdentityResult> UpdateAsync(IdentityRole role, CancellationToken cancellationToken)
        => throw new NotImplementedException();

    public Task<IdentityResult> DeleteAsync(IdentityRole role, CancellationToken cancellationToken)
        => throw new NotImplementedException();

    public Task<string> GetRoleIdAsync(IdentityRole role, CancellationToken cancellationToken)
        => throw new NotImplementedException();

    public Task<string> GetRoleNameAsync(IdentityRole role, CancellationToken cancellationToken)
        => throw new NotImplementedException();

    public Task SetRoleNameAsync(IdentityRole role, string roleName, CancellationToken cancellationToken)
        => throw new NotImplementedException();

    public Task<string> GetNormalizedRoleNameAsync(IdentityRole role, CancellationToken cancellationToken)
        => throw new NotImplementedException();

    public Task SetNormalizedRoleNameAsync(IdentityRole role, string normalizedName, CancellationToken cancellationToken)
        => throw new NotImplementedException();

    public Task<IdentityRole> FindByIdAsync(string roleId, CancellationToken cancellationToken)
        => throw new NotImplementedException();

    public Task<IdentityRole> FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken)
        => throw new NotImplementedException();
}


Startup.cs



この2つのクラスをDIできるように登録しておきます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddUserStore<MyUserStore>()
        .AddRoleStore<MyRoleStore>()
        .AddDefaultTokenProviders();

    // Add application services.
    services.AddTransient<IEmailSender, EmailSender>();

    services.AddMvc();
}

※ApplicationUser は IdentityUser の派生クラスとして、テンプレートで生成されたものをそのまま利用します。 IdentityRole は SDK(Microsoft.AspNetCore.Identity) 内で定義済みのものをそのまま利用します。



ここまでくれば動きます



インメモリなので、実行を止めずに Register → Log out → Log in と操作してみましょう。 実装した MyUserStore にブレークポイントを仕掛けてみると、どの様に動いているかわかり易いと思います。

テンプレートの Controller の実装を見ると、たくさん例示されています。起点となる SignInManager, UserManager, RoleManager の3つのインスタンスをDIで取得すれば、必要な機能へアクセスできるはずです。



ASP.NET Core Identity の全体像





主要な構造を全体的に書き出してみました。そして、今回は水色で書いたあたりを扱いました。その他の部分は必要に応じて試せると思いますが、また次回、これを元にしたカスタマイズをやってみたいと思います。



環境



Visual Studio 2017 (ver 15.5)
ASP.NET Core 2.0



参考情報



マイクロソフトの公式ドキュメントはこちら
https://docs.microsoft.com/en-us/aspnet/core/security/authentication/

弊社の配信事業本部でも Advent Calendar が行われております。
そちらにも ASP.NET Core Identity の記事がありますのでご紹介しておきます。
ASP.NET Core Identity ことはじめ 1


検索する

タグ

メタデータ

投稿のRSS