ハロの外部記憶インターフェイス

そろそろ覚える努力が必要かも…

ASP.NET MVC AD認証ログイン

注意

このポストはまだ成功確認が取れていない状態のメモです。 環境が整い次第テストを行う予定で、そのときのための準備になります。

AD認証でのログインについて調べる

Visual StudioのテンプレートではUseOpenIdConnectAuthenticationにより、OpenIdを利用したADログインになるため、 やりたいことの参考が出来なかったため、調べた事をメモする。

MVC Web ApplicationをMVCで認証なしで作成する。

プロジェクトにNugetから追加する。

  • Microsoft.Owin.Host.SystemWeb
    • OwinをIISで有効にするためのライブラリ
  • Microsoft.AspNet.Identity.Owin
    • 認証に必要なライブラリ

参照にPrincipalContextを使うためのライブラリを追加

  • System.DirectoryServices
  • System.DirectoryServices.AccountManagement

User

IUserを継承したユーザを作成する。 ただし、生成にはUserPrincpalから取得するようにする。

using Microsoft.AspNet.Identity;
using System.DirectoryServices.AccountManagement;
using System.Threading.Tasks;

public class AppUser : IUser<string>
{
    private UserPrincipal _adUser;

    public AppUser(UserPrincipal adUser)
    {
        this._adUser = adUser;
    }

    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<AppUser> manager)
    {
        var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
        return userIdentity;
    }

    #region IUser<string> Members

    public string Id => _adUser.SamAccountName;

    public string UserName
    {
        get => _adUser.SamAccountName;
        set { throw new System.NotImplementedException(); }
    }
    #endregion
}

UserStore

ユーザ関連のCLUD処理を行うStoreクラスを定義する。 PrincipalContextからFindByIdentityにより、ユーザの取得を行う。

using Microsoft.AspNet.Identity;
using System.DirectoryServices.AccountManagement;
using System.Threading.Tasks;

public class AppUserStore :
    IUserStore<AppUser>,
    IUserStore<AppUser, string>
{
    private readonly PrincipalContext _context;
    private AppUserStore(PrincipalContext context)
    {
        _context = context;
    }

    // context.Get<PrincipalContext>()でADのcontextを取得
    public static AppUserStore Create(IdentityFactoryOptions<AppUserStore> options, IOwinContext context)
    {
        //  PrincipalContextをOwinContextから取得するようにする。StartUpでCreatePerOwinContextに追加しておく
        //var principalContext = new PrincipalContext(ContextType.Domain);
        var principalContext = context.Get<PrincipalContext>();
        return new AppUserStore(principalContext);
    }

    #region IUserStore<MyUser, string> Members

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

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

    public void Dispose()
    {
        throw new NotImplementedException();
    }

    // UserPrincipal.FindByIdentityにより、検索する
    public Task<AppUser> FindByIdAsync(string userId)
    {
        var user = UserPrincipal.n(_context, userId);
        return Task.FromResult<AppUser>(new AppUser(user));
    }

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

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

    #endregion
}

UserMananger

UserStoreクラスの総裁を行うクラス

using Microsoft.AspNet.Identity;
using System.DirectoryServices.AccountManagement;
using System.Threading.Tasks;

public class AppUserMananger : UserManager<AppUser>
{
    private readonly PrincipalContext _context;

    private AppUserMananger(IUserStore<AppUser> store, PrincipalContext context) : base(store)
    {
        _context = context;
    }
    
    public static AppUserMananger Create(IdentityFactoryOptions<AppUserMananger> options, IOwinContext context)
    {
        var userStore = context.Get<AppUserStore>();
        var principalContext = context.Get<PrincipalContext>();
        return new AppUserMananger(userStore, principalContext);
    }

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

SignInManager

ログイン状態を管理するクラス

using Microsoft.AspNet.Identity;
using System.DirectoryServices.AccountManagement;
using System.Threading.Tasks;

public class AppSignInManager : SignInManager<AppUser, string>
{
    UserManager<AppUser, string> _manager;

    private AppSignInManager(UserManager<AppUser, string> userManager, IAuthenticationManager authenticationManager) 
        : base(userManager, authenticationManager)
    {
        _manager = UserManager;
    }

    public static AppSignInManager Create(IdentityFactoryOptions<AppSignInManager> options, IOwinContext context)
    {
        // UserManagerはOwinContextから取得する。
        AppUserMananger manager = context.GetUserManager<AppUserMananger>();
        return new AppSignInManager(manager, context.Authentication);
    }

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

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

        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;
    }
}

StartUp

using Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.AspNet.Identity;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using System.DirectoryServices.AccountManagement;
using System.Security.Claims;

[assembly: OwinStartup(typeof(MvcWeb.OwinStartUp))]
public class OwinStartUp
{
    public void Configuration(IAppBuilder app)
    {
        app.CreatePerOwinContext(() => new PrincipalContext(ContextType.Domain));
        app.CreatePerOwinContext<AppUserMananger>(AppUserMananger.Create);
        app.CreatePerOwinContext<AppSignInManager>(AppSignInManager.Create);
        
        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

        app.UseCookieAuthentication(new CookieAuthenticationOptions()
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Account/Login"),
            Provider = new CookieAuthenticationProvider
            {
                OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<AppUserMananger, AppUser>(
                    validateInterval: TimeSpan.FromMinutes(30),
                    regenerateIdentity: (manager,user) => user.GenerateUserIdentityAsync(manager))
            }
        });

    }
}

LoginViewModel

public class LoginViewModel
{
    [Required]
    [Display(Name = "ユーザID")]
    public string UserID { get; set; }

    [Required]
    [DataType(DataType.Password)]
    [Display(Name = "パスワード")]
    public string Password { get; set; }

    [Display(Name = "このアカウントを記憶する")]
    public bool RememberMe { get; set; }
}

AccountController

http://localhost/Acount/LoginからADのユーザ名、パスワードでログインする処理

[Authorize]
public class AccountController : Controller
{
    //
    // GET: /Account/Login
    [AllowAnonymous]
    public ActionResult Login(string returnUrl)
    {
        ViewBag.ReturnUrl = returnUrl;
        return View();
    }

    //
    // POST: /Account/Login
    [HttpPost]
    [AllowAnonymous]
    public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

       AppSignInManager signInManager = HttpContext.GetOwinContext().Get<AppSignInManager>();
       AppUserMananger userManager = HttpContext.GetOwinContext().GetUserManager<AppUserMananger>();

        // これは、アカウント ロックアウトの基準となるログイン失敗回数を数えません。
        // パスワード入力失敗回数に基づいてアカウントがロックアウトされるように設定するには、shouldLockout: true に変更してください。
        var result = await signInManager.PasswordSignInAsync(model.UserID, model.Password, model.RememberMe, shouldLockout: false);
        switch (result)
        {
            case SignInStatus.Success:
                return RedirectToLocal(returnUrl);
            case SignInStatus.LockedOut:
                return View("Lockout");
            case SignInStatus.RequiresVerification:
                return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
            case SignInStatus.Failure:
            default:
                ModelState.AddModelError("", "無効なログイン試行です。");
                return View(model);
        }
    }
    private ActionResult RedirectToLocal(string returnUrl)
    {
        if (Url.IsLocalUrl(returnUrl))
        {
            return Redirect(returnUrl);
        }
        return RedirectToAction("Index", "Home");
    }
}

Login View

@using MvcWeb.Models
@model LoginViewModel
@{
    ViewBag.Title = "ログイン";
}

<h2>@ViewBag.Title.</h2>
<div class="row">
    <div class="col-md-8">
        <section id="loginForm">
            @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
            {
                @Html.AntiForgeryToken()
                <h4>ローカル アカウントを使用してログインします。</h4>
                <hr />
                @Html.ValidationSummary(true, "", new { @class = "text-danger" })
                <div class="form-group">
                    @Html.LabelFor(m => m.UserID, new { @class = "col-md-2 control-label" })
                    <div class="col-md-10">
                        @Html.TextBoxFor(m => m.UserID, new { @class = "form-control" })
                        @Html.ValidationMessageFor(m => m.UserID, "", new { @class = "text-danger" })
                    </div>
                </div>
                <div class="form-group">
                    @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
                    <div class="col-md-10">
                        @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
                        @Html.ValidationMessageFor(m => m.Password, "", new { @class = "text-danger" })
                    </div>
                </div>
                <div class="form-group">
                    <div class="col-md-offset-2 col-md-10">
                        <div class="checkbox">
                            @Html.CheckBoxFor(m => m.RememberMe)
                            @Html.LabelFor(m => m.RememberMe)
                        </div>
                    </div>
                </div>
                <div class="form-group">
                    <div class="col-md-offset-2 col-md-10">
                        <input type="submit" value="ログイン" class="btn btn-default" />
                    </div>
                </div>
                <p>
                    @Html.ActionLink("新しいユーザーとして登録する", "Register")
                </p>
                @* これを有効にする前に、パスワード リセット機能に対するアカウント確認を有効にしてください。
                    <p>
                        @Html.ActionLink("パスワードを忘れた場合", "ForgotPassword")
                    </p>*@
            }
        </section>
    </div>
    <div class="col-md-4">
        <section id="socialLoginForm">
            @Html.Partial("_ExternalLoginsListPartial", new ExternalLoginListViewModel { ReturnUrl = ViewBag.ReturnUrl })
        </section>
    </div>
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}