最近の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関係?)