最近のASP.NET MVCでは昔ながらのForm認証ではなくASP.NET Identityという仕組みが使われるようになったようです。

これ、ユーザー登録・削除とかプロファイル管理とかもろもの機能も用意されてて、そりゃもー便利なんですが、VSのテンプレートではEntityFramework(DB)でユーザー管理するようになっています。

社内アプリとか作ってると、ユーザーの管理はActiveDirectoryでやってるで、認証機能だけあれば良いんだけど…MS親切過ぎっ、てこともあると思います。

AD使うなら「組織アカウント」なり「Winodws認証」なり使えそうな気がするのですが、大人の事情で使えないこともあったりするのです。

プロジェクトの準備

ASP.NET MVC5のプロジェクト作成時に認証方法をIndividual User Accountsにしとく。

後から変更できるかは不明。(できるんだろうけど大変そう)

参照設定でSystem.DirectoryServices.AccountManagementを追加。

ユーザーモデル ApplicationUser

ユーザーそのものを表現するクラスです。ユーザ名とかIDとか。

テンプレートではIdentityUserを継承していますが、IUserインターフェースを実装するように変更します。

(IdentityUserを継承しない時点でASP.NET Identityでは無いような気がしますが、気にしないでおきます)

ActiveDirectoryのユーザー情報はここにコンポジションしてます。

Id,NameともにSamAccountNameを使います。

GenerateUserIdentityAsyncメソッドはCookie関係の処理で必要そうなので残しときます。

\Models\IdentityModels.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ApplicationUser : IUser
    {
        public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
        {
            // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
            var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
            // Add custom user claims here
            return userIdentity;
        }

        public ApplicationUser(UserPrincipal ADUser) {
            this.mADUser = ADUser;
        }

        private UserPrincipal mADUser;
        #region IUser<string> Members
        public string Id {
            get{
                return mADUser.SamAccountName;
            }
        }
        public string UserName {
            get{
                return mADUser.SamAccountName;
            }
            set {
                throw new System.NotImplementedException();
            }
        }
        #endregion
    }

ユーザーストア ApplicationUserStore

ユーザーの操作(CRUD)を担当するクラスです。

テンプレートだとIdentityDbContextを継承してDBとのやり取りをうまいことしてくれるようですが、IUserStoreを実装してADを読むようにします。

追加・変更・削除はAD上でやるので、読み取りのみの実装になります。

ADへの接続情報にあたるPrincipalContextはOwinのIoC的な機能を利用します(PrincipalContextは\App_Start\Startup.Auth.csでOwinのコンテキストに登録→context.Getで取得)

ユーザー読み取りの肝はUserPrincipal.FindByIdentityです。

\Models\IdentityModels.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class ApplicationUserStore : IUserStore<ApplicationUser>
    {
        private readonly PrincipalContext _context;
        private ApplicationUserStore(PrincipalContext context) {
            this._context = context;
        }
        // PrincipalContext will be set by Owin.
        public static ApplicationUserStore Create(IdentityFactoryOptions<ApplicationUserStore> options, IOwinContext context) {
            var principal_context = context.Get<PrincipalContext>();
            var store = new ApplicationUserStore(principal_context);
            return store;
        }

        #region IUserStore<ApplicationUser,string> Members

        public Task CreateAsync(ApplicationUser user) {
            throw new System.NotImplementedException();
        }

        public Task DeleteAsync(ApplicationUser user) {
            throw new System.NotImplementedException();
        }

        public Task<ApplicationUser> FindByIdAsync(string userId) {
            var user=UserPrincipal.FindByIdentity(_context, userId);
            return Task.FromResult<ApplicationUser>(new ApplicationUser(user));
        }

        public Task<ApplicationUser> FindByNameAsync(string userName) {
            var user = UserPrincipal.FindByIdentity(_context, userName);
            return Task.FromResult<ApplicationUser>(new ApplicationUser(user));
        }

        public Task UpdateAsync(ApplicationUser user) {
            throw new System.NotImplementedException();
        }

        #endregion

        #region IDisposable Members

        public void Dispose() {
        }

        #endregion
    }

ユーザーマネージャApplicationUserManager

ユーザーマネージャはユーザーに対するいろんな機能を提供するクラスみたいです。

テンプレートを見るとパスワードポリシーとか二要素認証とかユーザーのロックルールとかいろいろな機能が提供されています。

ユーザーストアがDAOだとするとユーザーマネージャはビジネスロジックにあたる部分でしょうか。

便利機能はさっくり消して、パスワードチェックを実装します。

App_Start\IdentityConfig.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class ApplicationUserManager : UserManager<ApplicationUser>
    {
        private readonly PrincipalContext _context;
        public ApplicationUserManager(IUserStore<ApplicationUser> store, PrincipalContext context)
            : base(store)
        {
            this._context = context;
        }

        public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
        {
            var user_store = context.Get<ApplicationUserStore>();
            var principal_context=context.Get<PrincipalContext>();
            var manager = new ApplicationUserManager(user_store, principal_context);
            return manager;
        }

        public override async Task<bool> CheckPasswordAsync(ApplicationUser user, string password) {
            return await Task.FromResult(_context.ValidateCredentials(user.UserName, password, ContextOptions.Negotiate));
        }
    }

パスワードチェックにはCheckPasswordAsyncをオーバーライドすればOKみたいです。

このCheckPasswordAsync、MSの日本語ドキュメントには出てません。日本語のドキュメントはASP.NET Identity 1.0で止まってるんですかね?

英語ドキュメントには出てました。

サインインマネージャApplicationSignInManager

ユーザーマネージャとの棲み分けがよくわからないんですが、サインインマネージャでログイン状態を管理してるようです。Controllerからはこのクラスを使います。

テンプレートそのままでいけるかと思いきや、コントローラのLoginメソッドでPasswordSignInAsyncをコールすると、Store does not implement IUserLockoutStoreって怒られます。

どうもUserStoreにはIUserStore以外にもいろいろ実装しないといけないようです。が、面倒なので、SignInManagerの方で対応します。

でことでPasswordSignInAsyncをオーバーライド。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ApplicationSignInManager : SignInManager<ApplicationUser, string>
    {
        public ApplicationSignInManager(ApplicationUserManager userManager, IAuthenticationManager authenticationManager)
            : base(userManager, authenticationManager)
        {
        }

        public override Task<ClaimsIdentity> CreateUserIdentityAsync(ApplicationUser user)
        {
            return user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager);
        }

        public static ApplicationSignInManager Create(IdentityFactoryOptions<ApplicationSignInManager> options, IOwinContext context)
        {
            return new ApplicationSignInManager(context.GetUserManager<ApplicationUserManager>(), context.Authentication);
        }

        public async override Task<SignInStatus> PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout) {
            var result = SignInStatus.Failure;  //default fail

            try {
                var user = await this.UserManager.FindAsync(userName, password);
                if (user != null) {
                    await this.SignInAsync(user, isPersistent, true);
                    result = SignInStatus.Success;
                }
            } catch {
                result = SignInStatus.Failure;
            }
            return result;
        }
    }

ロックアウトとかその辺はさっくり省略しています。

認証コンポーネントの登録

Owinが若干理解できてないんですが、App_Start\Startup.Auth.csのStartupクラスで認証関連のコンポーネントの登録をするようです。

こちらにも外部ログインとか二要素認証なんかのコードがあったので、さっくり削除。

App_Start\Startup.Auth.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public partial class Startup
    {
        // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
        public void ConfigureAuth(IAppBuilder app)
        {
            // Configure the db context, user manager and signin manager to use a single instance per request
            app.CreatePerOwinContext(() => new PrincipalContext(ContextType.Domain));   //ADD
            app.CreatePerOwinContext<ApplicationUserStore>(ApplicationUserStore.Create);    //modify
            app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
            app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);

            // Enable the application to use a cookie to store information for the signed in user
            // and to use a cookie to temporarily store information about a user logging in with a third party login provider
            // Configure the sign in cookie
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Account/Login"),
                Provider = new CookieAuthenticationProvider
                {
                    // Enables the application to validate the security stamp when the user logs in.
                    // This is a security feature which is used when you change a password or add an external login to your account.
                    OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                        validateInterval: TimeSpan.FromMinutes(30),
                        regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
                }
            });
        }
    }

ログインフォーム変更

いわゆるView部の変更です。ユーザーModelの変更と不要な部分を削除が主な作業。

emailをuseridにします。

モデル(\Models\AccountViewModels.cs)

ログインフォーム用モデルを変更します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class LoginViewModel
    {
        [Required]
        [Display(Name = "User ID")]
        public string UserID { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }

        [Display(Name = "Remember me?")]
        public bool RememberMe { get; set; }
    }

例によって便利機能関係のViewModelは削除。(LoginViewModelのみ残しました)

ビュー(\Views\Account\Login.cshtml)

モデルに合わせて変更します。

外部ログイン用のコンテンツ(_ExternalLoginsListPartial)も削除

Login.cshtml以外のcshtmlは消しました。

コントローラー(Controllers\AccountController.cs)

Login/LogOff以外のメソッドをざっくり削除。

残したのは

メソッド:コンストラクタ/Login/LogOff/Dispose/RedirectToLocal

プロパティ:SignInManager/UserManager/AuthenticationManager

LoginメソッドのPasswordSignInAsync部分をModelに合わせて書き換えます(Email→UserID)

1
var result = await SignInManager.PasswordSignInAsync(model.UserID, model.Password, model.RememberMe, shouldLockout: false);

長い

http://localhost:????/Account/Loginから、ADのユーザー名、パスワードでログインできます。

う~ん、もともとの多機能テンプレートもいいけど、ADで(+Formで)認証とか需要あると思うんで、もっと簡単にできないもんでしょうかね~。

もしかしたら続くかも(Role関係?)