ASP.NET identityの実装 ログイン機能

# ASP.NET Identiyのログイン機能実装

データベースから生成したADO.NET Entity Data Model を使用するにはどうしたら良いんだろうと思ったので実装してみた。
したいことは出来たのでメモ。  

* ASP.NET Identiyを使用した認証。  
* 認証用のユーザテーブルはすでにあるものとする。  
* ビューの生成にレイアウトを使用しない。  
* 実行しながら確認したいので画面側から作成。  
* プロジェクト名は"IdenditySample"。

**環境**

* ASP.NET MVC 4.5  
* ASP.NET Identity 2.2.1  
* EntityFramework 6.2  

**作成する認証のクラス**

* `AppUser : IUser` ユーザー情報の定義クラス。  
* `AppUserStore : IUserStore,IUserPasswordStore` ユーザー、パスワードのCRUD処理を行う。  
* `AppUserManager : UserManager`  自動的に AppUserStore に変更を保存する api に関連するユーザーを公開する。  
* `AppSignInManager : SignInManager` ログイン情報の管理を行う。  
* `AppPasswordHasher : IPasswordHasher` FindAsyncでパスワード比較を行うPasswordHasherの入れ替えをする。https://www.cat-ears.net/?p=31697  

## ログイン後の画面を作成

まずはログインされたら表示される画面を用意。
`RouteConfig`の設定に従って`/Home/Index`を作成する。

`Controllers/HomeController.cs`を作成する。
自動で作成されたクラス`Index`に`Authorize`属性を追加する。
この属性によりログイン認証が行われていない場合、ログイン画面に飛ぶ。

```csharp
[Authorize]
public class HomeController : Controller
{
    // GET: Home
    public ActionResult Index()
    {
        return View();
    }
}
```

ビュー側を作成する。
こちらは"Hello world."とログインユーザー名(@User.Identity.Name)を記載した。

```html
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>
    <div> 
        <div>Hello world.</div>
        <div>@User.Identity.Name</div>
    </div>
</body>
</html>
```

現段階で実行すると認証されていないのでhttp 401になる。

## 参照の追加

ASP.NET Identityを使用するため
NuGetで以下のパッケージを追加する。

* **Microsoft.AspNet.Identity.Owin**  
* **Microsoft.Owin.Host.SystemWeb**  
    IISでOWINを動かすのに必要。

`.ja`が付いてる日本語リソースでもOK。

ビルドすると以下のエラーになる。  

> OwinStartupAttribute を含むアセンブリが見つかりませんでした。
> Startup クラスまたは [AssemblyName].Startup クラスを含むアセンブリが見つかりませんでした。

## 初期化コードの追加

エラー箇所の`/Startup.cs`を作成する。

```csharp
public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            //クッキーベースの認証
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            //ログイン時のパスを設定
            LoginPath = new PathString("/Account/Login")
        });
    }
}
```

これで実行すると`Startup`に起因するエラーは解消されるが、`/Account/Login`がないのでhttp 404になる。

## ログイン画面の作成。

`Startup`で設定した`/Account/Login`を作成する。
`Controllers/AccountController.cs`を作成してユーザーとパスワードのフォームを作成する。

```csharp
[AllowAnonymous]
public class AccountController : Controller
{
    // GET: Account
    public ActionResult Login()
    {
        return View(new LoginViewModel());
    }

    // POST
    [HttpPost]
    public ActionResult Login(LoginViewModel m)
    {
        // ログインしたらルートへ戻る。
        return Redirect("/");
    }
}

/// <summary>
/// ViewModel
/// </summary>
public class LoginViewModel
{
    [Display(Name = "ユーザー名")]
    public string UserName { get; set; }

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

ViewModelはきちんとディレクトリ分けしたほうが良いが、めんどくさいので一緒に記載。
また、きちんとルーティングするならReturnURLを受け付けて`Redirect()`に与える方がいい。
次に画面側。

```html
@model IdenditySample.Controllers.LoginViewModel

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Login</title>
</head>
<body>
    <div> 
        @using (Html.BeginForm())
        {
            <div>
                @Html.LabelFor(h => h.UserName)
                @Html.TextBoxFor(h => h.UserName)
            </div>
            <div>
                @Html.LabelFor(h => h.Password)
                @Html.PasswordFor(h => h.Password)
            </div>
            <input type="submit" value="Login" />
        }
    </div>
</body>
</html>
```

これで実行してもエラーにならなくなる。
ログイン画面が表示されてボタンを押すと、認証されていないのでやっぱりログイン画面に戻る。
次に認証処理を実装する。

## ユーザー情報の定義作成

**/Models/AuthModels.cs**を作成

```csharp
public class AppUser : IUser<string>
{
    public string Id { get; set; }
    public string UserName { get; set; }
    // Identityはハッシュ化したパスワードを使用するらしい
    public string HashPassword { get; set; }
}
```

## ユーザ情報のCRUD処理を実装

ここではユーザー情報を返す処理だけ実装する。
`SignInManager`,`UserManager`で管理させるため、`IUserStore`,`IUserPasswordStore`のインターフェースを実装する。

以下を`/Models/AuthModels.cs`に追加。

```csharp
public class AppUserStore : IUserStore<AppUser>, IUserStore<AppUser, string>, IUserPasswordStore<AppUser, string>
{
・・・省略
    public void Dispose() { }

    public Task<AppUser> FindByNameAsync(string userName)
    {
        //ここでログインユーザー情報を見つけて返す処理。
        if ("user" == userName)
        {
            var user = new AppUser
            {
                Id = "1",
                UserName = "user",
                HashPassword = "user"
            };
            return Task.FromResult(user);
        }
        AppUser result = null;
        return Task.FromResult(result);
    }

    public Task<string> GetPasswordHashAsync(AppUser user)
    {
        // 単純にパスワードを返す。
        return Task.FromResult(user.HashPassword);
    }
・・・省略
}
```

`FindByNameAsync`は入力されたユーザ名が"user"であるときユーザー情報を返すように実装。
実際には、DB等から検索してユーザー情報を返すなどの実装をする。
実装例->[ASP.NET Identityの実装 DBからユーザ情報を取得する](https://qiita.com/Sa_Ru/items/632330ab50e3b60242c6)
作成、削除、変更などの機能は今回使用しないのでスルーする状態になっている。

## AppUserStoreを管理するクラスの実装

`UserManager`,`SignInManager`を実装する。
`AppUserStore`と同じ`/Models/AuthModels.cs`に追加。

```csharp
public class AppUserManager : UserManager<AppUser>
{
    public AppUserManager(IUserStore<AppUser> store) : base(store)
    {
    }

    public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options, IOwinContext context)
    {
        var manager = new AppUserManager(new AppUserStore());
        return manager;
    }
}

public class AppSignInManager : SignInManager<AppUser, string>
{
    public AppSignInManager(UserManager<AppUser, string> userManager, 
        IAuthenticationManager authenticationManager) : base(userManager, authenticationManager)
    {
    }

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

## ユーザー管理クラスのインスタンス登録

作成した`AppSignInManager`,`AppUserManager`,を連携させるため登録をする。
以下を`Startup.cs`に追加する。

```csharp
public void Configuration(IAppBuilder app)
{
    app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);
    app.CreatePerOwinContext<AppSignInManager>(AppSignInManager.Create);

    app.UseCookieAuthentication(new CookieAuthenticationOptions
・・・省略
}
```

これでASP.NET Identityを使用するためのロジックを実装出来た。

## Controllerにログイン処理を実装

作成した認証処理を使用するための実装を行う。
`/Controller/AccountController.cs`のHTTP POST処理を変更。

```csharp
// POST
[HttpPost]
public async Task<ActionResult> Login(LoginViewModel m)
{
    if (m.UserName == null || m.Password == null) return View();
    // UserManagerを使用してログイン可能か判断
    var manager = this.HttpContext.GetOwinContext().GetUserManager<AppUserManager>();
    var user = await manager.FindAsync(m.UserName, m.Password);
    if (user == null) return View(m);

    // ユーザーの識別情報を作成し、ログインする
    var identity = manager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
    var authenticationManager = HttpContext.GetOwinContext().Authentication;
    authenticationManager.SignIn(identity);

    // ログインしたらルートへ戻る。
    return Redirect("/");
}
```

確認のため実行すると`FindByNameAsync`で設定した"user"を入力しても`var user = await manager.FindAsync(m.UserName, m.Password);`でnullになることがわかる。

これは`FindAsync`内でパスワードの比較をHashに変換して比較しているからで、`IPasswordHasher`を使用してパスワード比較の方法を変更する。

## パスワード比較方法の変更

`IPasswordHasher`のインターフェイスを実装する。

```csharp
public class AppPasswordHasher : IPasswordHasher
{
    public string HashPassword(string password)
    {
        // ハッシュ化しないでそのまま返す
        return password;
    }

    public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
    {
        // 単純に文字列を比較
        if (hashedPassword == HashPassword(providedPassword))
        {
            return PasswordVerificationResult.Success;
        }
        else
        {
            return PasswordVerificationResult.Failed;
        }
    }
}
```

実装した`AppPasswordHasher`を`AppUserManager`に登録する。
追加した`AppUserManager`の`Create`に以下を追加する。

```csharp
public class AppUserManager : UserManager<AppUser>
{
    public AppUserManager(IUserStore<AppUser> store) : base(store)
    {
    }

    public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options, IOwinContext context)
    {
        var manager = new AppUserManager(new AppUserStore());
        // デフォルトのパスワード管理はパスワードをハッシュ化して扱う。
        // 平文のパスワードを扱うためにPasswordHasherを変更。
        manager.PasswordHasher = new AppPasswordHasher();
        return manager;
    }
}
```

## 確認

> `/Account/Login`
↓
ログイン -> 失敗 -> `/Account/Login`
↓
成功
↓
`/Home/Index`

ログイン画面からログイン後Home/Indexに移動することが確認できる。
また、Home/Indexに直接URL指定してもログインしていなければログイン画面に移動する。

# まとめ

`AppUserManager`のに`IPasswordHasher`を実装しなおしているが、
`FindAsync`をオーバーライドしてパスワードを比較するようにすれば良いのかも。

[UserManager.FindAsync to use custom Userstore's FindByIdAsync method instead of FindByNameAsync](https://stackoverflow.com/questions/36690729/usermanager-findasync-to-use-custom-userstores-findbyidasync-method-instead-of)

あと、AccountControllerのPOSTメソッド内のログイン可能確認とサインイン処理はモデル側の実装かなと思う。


# 参考

* [ASP.NET Identityカスタマイズに挑戦](http://blog.okazuki.jp/entry/2013/11/10/190811)
* [ASP.NET Identityのカスタマイズ－既存のユーザー情報を使っての認証](http://kiyokura.hateblo.jp/entry/2014/06/23/010749)
* [[めも]ASP.NET Identity 2.0.0 カスタマイズ](https://www.cat-ears.net/?p=31697)