きよくらの備忘録

「三日坊主と呼ばせない!日記」改め。主にソフトウェア開発関連の話題。

ASP.NET Identityのカスタマイズ-既存のユーザ情報を使っての認証

ASP.NET MVC 5になって、認証のインフラとしてASP.NET Identitiyが登場しました。今までのMembership Providerと比べてかカスタマイズもし易く、使い勝手の良いところが素敵です。

ちょうど、『既に別システムで運用されている独自のユーザー情報をそのまま利用し、アプリケーションのユーザー認証を行う』という要件で実装する機会がありました。せっかくなので要点を抜粋したサンプルをgithubにおいてみました。

kiyokura/AspNetIdentityCustomStoreSample

 

 

以下に、ポイントを軽く紹介してみたいと思います。

概要

基本的には、@okazuki さんの ASP.NET Identityカスタマイズに挑戦 で取り上げられているサンプルそのままです。ですので、差分となるポイントを中心に(というかそこだけ)を紹介しようと思います。

ポイントとなるのは

  1. 独自のユーザー情報クラスの作成
  2. 独自のユーザーストアクラスの作成
  3. 独自のパスワードハッシュアルゴリズムの実装
  4. UserManagerで利用するパスワードハッシュアルゴリズムを設定

です。

@okazuki さんのサンプルに対して追加されているのが、特に『3. 独自のパスワードハッシュアルゴリズムの実装』となっています。 また、既に存在するデータを使って認証を行うだけなので、ユーザーの登録に必要な処理などは実装していません(そのあたりは既存の別の管理ツールなどがある想定)。

 

 

独自のユーザー情報の例

既に別システムで利用されているユーザ情報の例として、以下のようなテーブルに格納されているものを想定します。

CREATE TABLE [dbo].[UserInfo] (
    [UserID]    UNIQUEIDENTIFIER NOT NULL,
    [LoginCode] NVARCHAR (50)    NOT NULL,
    [FirstName] NVARCHAR (50)    NOT NULL,
    [LastName]  NVARCHAR (50)    NOT NULL,
    [EMail]     NVARCHAR (MAX)   NOT NULL,
    [Password]  NVARCHAR (MAX)   NOT NULL,
    PRIMARY KEY CLUSTERED ([UserID] ASC)
);
GO
CREATE UNIQUE NONCLUSTERED INDEX [IX_UserInfo_U1]
    ON [dbo].[UserInfo]([LoginCode] ASC);

また、Passwordには、DBのユーザー関数として定義されている独自のアルゴリズムでハッシュ化されていることを想定しています。

 

独自のユーザー情報クラスの作成

IUser の実装として、MyAppUser という名前で作りました。基本的にはDBのテーブルの情報をそのまま読み込むつもりの構成です。ハッシュ化されたパスワードもここで保持するようにしておきます。

/// サンプルプロジェクト:Models/Auth/Auth.csに実装
public class MyAppUser : IUser
{
    /// <summary>
    /// DB上の一意キー
    /// </summary>
    public string Id { get; set; }

    /// <summary>
    /// ログイン認証に使うユーザーID
    /// </summary>
    public string UserName { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string EMail { get; set; }

    /// <summary>
    /// ハッシュ化されてDBに格納されているパスワード
    /// </summary>
    public string HashedPassword { get; set; }
}

 

独自のユーザーストアクラスの作成

カスタムストアを実装します。今回はログイン認証に必要最低限の機能を実現するため、IUserStore と IUserPasswordStore のみの実装とします。処理を実装するメソッドも、とりあえず必要最低限の2つのみにしています。

/// サンプルプロジェクト:Models/Auth/Auth.csに実装
using Dapper;

~

public class MyAppUserStore : IUserStore<MyAppUser>, IUserPasswordStore<MyAppUser>
{
    public Task<MyAppUser> FindByNameAsync(string userName)
    {
        // DBを検索する。Entitiy Franewrokを使っても良い。今回は諸事情と好みの問題でDapperを利用。
        using (var cn = DbConnectionFactory.Create("DefaultConnection"))
        {
            cn.Open();
            var sql = " SELECT " +
                      "   Convert(nvarchar(MAX), UserID) AS ID , LoginCode AS UserName , FirstName , LastName , EMail , Password AS HashedPassword " +
                      " FROM " +
                      "   UserInfo " +
                      " WHERE " +
                      "   LoginCode = @UserName ";

            var users = cn.Query<MyAppUser>(sql, new { UserName = userName });
            return Task.FromResult(users.FirstOrDefault());
        }
    }

    public Task<string> GetPasswordHashAsync(MyAppUser user)
    {
        // DBから取得されたMyAppUserにはハッシュ化されたパスワードが既に格納されているはず。
        // なのでここではそれをそのまま返す
        return Task.FromResult(user.HashedPassword);
    }

    public Task CreateAsync(MyAppUser user)
    {
        throw new NotImplementedException();
    }

    public Task DeleteAsync(MyAppUser user)
    {
        throw new NotImplementedException();
    }

    public Task<MyAppUser> FindByIdAsync(string userId)
    {
        throw new NotImplementedException();
    }

    public Task UpdateAsync(MyAppUser user)
    {
        throw new NotImplementedException();
    }

    public void Dispose()
    {
        //例外は出ないようにNotImplementedExceptionは消しておく
    }

    public Task<bool> HasPasswordAsync(MyAppUser user)
    {
        throw new NotImplementedException();
    }

    public Task SetPasswordHashAsync(MyAppUser user, string passwordHash)
    {
        throw new NotImplementedException();
    }
}

 

独自のパスワードハッシュアルゴリズムの実装

ASP.NET Identity では 標準のパスワードハッシュアルゴリズムとして PasswordHasher クラスが用意されています。 しかし、今回のように別システムで独自のアルゴリズムによってハッシュ化されている場合は、当然これではうまくいきません。 その場合は、IPasswordHasher の実装としてHasherを作ることで解決できます。以下は、DB側で実装されたユーザー定義関数を利用してハッシュ化されたパスワードを取得するカスタム・ハッシャーの例です。

/// サンプルプロジェクト:Models/Auth/Auth.csに実装
public class MyPasswordHasher : IPasswordHasher
{
    public string HashPassword(string password)
    {
        using (var cn = DbConnectionFactory.Create("DefaultConnection"))
        {
            cn.Open();
            var sql = "SELECT dbo.HashPassword(@RawPassword)";
            return cn.Query<string>(sql, new { RawPassword = password }).Single();
        }
    }

    public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
    {
        if (hashedPassword == this.HashPassword(providedPassword))
        {
            return PasswordVerificationResult.Success;
        }
        else
        {
            return PasswordVerificationResult.Failed;
        }
    }
}

 

UserManagerで利用するパスワードハッシュアルゴリズムを設定

作成したカスタム・ハッシャーをUserManagerが利用するように設定します。といっても、UserManagerのPasswordHasher プロパティーにインスタンスを設定してあげるだけです。こんな感じで設定してやれば、あとは勝手に使ってくれます。

/// サンプルプロジェクト: Controllers/AuthController.csに実装
//ログインを処理するコントローラのActionメソッド
[HttpPost]
[AllowAnonymous]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        // カスタマイズのポイント:
        //   UserManagerのインスタンス作成時、
        //     型引数:カスタマイズしたユーザー情報
        //     コンストラクタの引数:カスタマイズしたユーザストアのインスタンス
        //   をそれぞれ渡す
        var userManager = new UserManager<MyAppUser>(new MyAppUserStore());

        // カスタマイズのポイント:
        //   パスワードのハッシュ化アルゴリズムとして、IPasswordHasherを実装したカスタムクラスのインスタンスを設定
        userManager.PasswordHasher = new MyPasswordHasher();

        var user = await userManager.FindAsync(model.UserName, model.Password);
        if (user != null)
        {
            var authentication = this.HttpContext.GetOwinContext().Authentication;
            var identify = await userManager.CreateIdentityAsync(
                user,
                DefaultAuthenticationTypes.ApplicationCookie);
            authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
            authentication.SignIn(new AuthenticationProperties() { IsPersistent = false }, identify);

            return Redirect(returnUrl);
        }
        else
        {
            ModelState.AddModelError("", "ログインIDまたはパスワードが無効です。");
        }
    }
    
    // ここで問題が発生した場合はフォームを再表示します
    return View(model);
}

 

別のカスタマイズ方法も…

今回は UserManager はそのまま利用し、ユーザーストアをカスタマイズする方法でやってみました。

しかし、ユーザー認証処理そのものがDB側でストアドプロシージャによって行われ、アプリ開発者にそのロジックが公開されていないようなケースもあると思います。 その場合はユーザーストアのカスタマイズで対応するのでは無く、、UserManager を継承したカスタムのUserManager を作成し、FindAsyncメソッドをオーバーライドしてやるほうが簡単かもしれません。

 

まとめ

ということで、簡単ですがこんな感じで既存の独自構成のユーザ情報を使っての認証を実装してみました。特にエンタープライズアプリケーションだと、社内で別途管理統一されたユーザーデータを使って個々のアプリケーションで認証を行う…という事も少なくないのではないかと思っています。そういった場合でも、ASP.NET Identityでは比較的簡単なカスタマイズで対応できると思います。 また今回は読み込みのみですが、このテーブルに対してユーザの登録やパスワードの変更処理などをカスタマイズして実装することも容易だと思います。

 

なお、今回は下記の情報を参考にさせていただきました。