Cookie 認証でログイン・ログアウト処理を作る【ASP.NET Core】

ASP.NET CoreでCookie認証を使ってログイン・ログアウト機能を実装してみます。
`Microsoft.AspNetCore.Authentication`を使うと簡単に行うと簡単に実装できるようです。


**[[Microsoft.AspNetCore.Authentication - Nuget]](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication/)**

.NET Core SDKのバージョンは以下の通りです。

```bash
$ dotnet --version
2.1.502
```

## サンプルプロジェクトの作成

今回はRazorページのテンプレートを使いたいと思います。Visual Studioでは以下の操作でプロジェクトを開始できます。
`新規作成` > `プロジェクト` >`新しいプロジェクト` > `Web` > `ASP.NET Core Web アプリケーション` > `Web アプリケーション`
.NET Core CLIで作成する場合は以下のコマンドです。

```bash
$ dotnet new razor -n "プロジェクト名"
```

完成後のソースはこちらのリポジトリに置きました。
**https://github.com/sano-suguru/netcore-app-with-cookie-auth**
ところどころ端折っているので実際のコードは上記のリンクからご確認ください。

## クッキー認証ミドルウェアの設定

`Startup.cs`に認証ミドルウェアを追加します。
その際、Cookieに、セッションハイジャック等を防ぐために`httpOnly`属性（JSから触れなくする）、`Secure`属性（SSL時のみクッキーを送信）を設定します。

`ConfigureServices`メソッドに以下の通り設定を追加。

```diff:Startup.cs
  services.Configure<CookiePolicyOptions>(options => {
    options.CheckConsentNeeded = context => !context.User.Identity.IsAuthenticated;
    options.MinimumSameSitePolicy = SameSiteMode.None;
+   options.Secure = CookieSecurePolicy.Always;
+   options.HttpOnly = HttpOnlyPolicy.Always;
  });
+ services
+   .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
+   .AddCookie();
```

`Configure`メソッドに以下の通り設定を追加。

```diff:Startup.cs
    app.UseHttpsRedirection();
    app.UseStaticFiles();
+   app.UseCookiePolicy();
+   app.UseAuthentication();
    app.UseMvc();
  }
```

## ログインページ、ログイン処理を作成する

以下のキャプチャのログインページを作ります。
![キャプチャ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195024/939adba1-3133-cfd0-6b29-99fe98f65a04.png)

`/Pages/Account/`ディレクトリに`Login.cshtml`と`Login.cshtml.cs`を作成します。
ログインが必要なページに未ログイン状態でアクセスした場合に自動でリダイレクトされるデフォルトのパスが`/Account/Login`であるためです。

```html:/Pages/Account/Login.cshtml
@page
@model LoginModel
@{
    ViewData["Title"] = "Login";
}
<h2>Login</h2>
<form method="post">
    <div class="form-group">
        <label asp-for="Input.Email">Email address</label>
        <input asp-for="Input.Email" class="form-control" placeholder="Enter email">
    </div>
    <div class="form-group">
        <label asp-for="Input.Password">Password</label>
        <input asp-for="Input.Password" class="form-control" placeholder="Password">
    </div>
    <div class="form-check">
        <input asp-for="Input.RememberMe" class="form-check-input">
        <label asp-for="Input.RememberMe" class="form-check-label">Remember Me</label>
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
</form>
```

`cshtml`ではタグヘルパー`asp-for`で`LoginModel`クラスのインナークラス`InputModel`にフォームの値をバインドしています。

```csharp:/Pages/Account/Login.cshtml.cs
[BindProperty]
public InputModel Input { get; set; }

public class InputModel {
  [Required]
  [EmailAddress]
  public string Email { get; set; }

  [Required]
  [DataType(DataType.Password)]
  public string Password { get; set; }

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

以下、`Login.cshtml.cs`の`LoginModel`クラスからログイン処理を抜粋します。
サンプルのため処理べた書きです。実際にはDBアクセス処理やエラー表示が必要になると思います。

```csharp:Login.cshtml.cs
// submitボタンがクリックされてpostリクエストを受け付けたときに動く
public async Task<IActionResult> OnPostAsync(string returnUrl = null) {
  // 必須入力がないなどの場合ログインさせない。（ログインページに戻る）
  if (!ModelState.IsValid) return Page();

  // ==================================================
  // ダミーの認証処理（実際にはDBを使います）
  var mockDB = new[] {
    (email: "nossa@example.com", password: "pazzword1"),
    (email: "nyaan@example.com", password: "pazzword2")
  };
  bool isValid = mockDB.SingleOrDefault(x => x.email == Input.Email && x.password == Input.Password) != null;
  // ===================================================
  // Eメール・パスワードが間違っている場合ログインさせない。
  if (!isValid) return Page();
  
  // ★以下ログイン処理
  // 名前、電子メール アドレス、年齢、Sales ロールのメンバーシップなど、id 情報の一部
  Claim[] claims = {
    new Claim(ClaimTypes.NameIdentifier, Input.Email), // ユニークID
    new Claim(ClaimTypes.Name, Input.Email),
  };

  // 一意の ID 情報
  var claimsIdentity = new ClaimsIdentity(
    claims, CookieAuthenticationDefaults.AuthenticationScheme);

  // ログイン
  await HttpContext.SignInAsync(
    CookieAuthenticationDefaults.AuthenticationScheme,
    new ClaimsPrincipal(claimsIdentity),
    new AuthenticationProperties {
      // Cookie をブラウザー セッション間で永続化するか？（ブラウザを閉じてもログアウトしないかどうか）
      IsPersistent = Input.RememberMe
    });
  return LocalRedirect(returnUrl ?? Url.Content("~/"));
}
```




## ログアウトページを作成する

以下のキャプチャのログアウトページを作ります。
![キャプチャ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195024/0a152e96-2b10-3041-58b4-6b5ee6b4c40e.png)


```html:Account/Logout.cshtml
@page
@model AuthWithCookie.Pages.Account.LogoutModel
@{
    ViewData["Title"] = "Logout";
}
<h2>Logout</h2>
<form method="post">
    <button type="submit" class="btn btn-primary">Log out</button>
</form>
```

ログアウトは本当に簡単で`HttpContext.SignOutAsync`を呼ぶだけです。

```Account/Logout.cshtml.cs
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Threading.Tasks;

namespace AuthWithCookie.Pages.Account {
  public class LogoutModel : PageModel {
    public async Task<IActionResult> OnPostAsync() {
      await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
      // ログアウト後はトップページへリダイレクト
      return LocalRedirect(Url.Content("~/"));
    }
  }
}
```

## ナビゲーションバーにログイン・ログアウトページを登録する

ログイン・ログアウトページにナビゲーションバーからアクセスできるようにします。
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195024/76410842-8e78-e162-b0eb-5c09cf4ca05e.png)


```diff:/Pages/Shared/_Layout.cshtml
  <div class="navbar-collapse collapse">
      <ul class="nav navbar-nav">
          <li><a asp-page="/Index">Home</a></li>
          <li><a asp-page="/About">About</a></li>
          <li><a asp-page="/Contact">Contact</a></li>
+         <li><a asp-page="/Account/Login">Log in</a></li>
+         <li><a asp-page="/Account/Logout">Log out</a></li>
      </ul>
  </div>
```

## ログイン済みでないと閲覧できないページを作る

`Cotact`ページをログイン者専用のコンテンツにしてみます。
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195024/4b14e289-ce4a-1578-127c-48a7436d3ea2.png)

`[Authorize]`属性をコントローラークラスに付けるとログイン済みでないとそのコントローラーにアクセスできなくなります。また、アクションメソッドごとに個別に付けることも可能です。


```csharp:/Pages/Contact.cshtml.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace AuthWithCookie.Pages {
  [Authorize]
  public class ContactModel : PageModel {
    public string Message { get; set; }

    public void OnGet() => Message = "Your contact page.";
  }
}
```

ログインしていない状態でナビゲーションバーのContactをクリック（`/Contact`にアクセス）するとログインページ`/Account/Login`に自動でリダイレクトします。
ログインするとログインページに直接アクセスした場合はトップへ、未ログイン状態でログインが必要なページにアクセスしログインページにリダイレクトされた場合にはそのページへ戻ります。

```csharp
 return LocalRedirect(returnUrl ?? Url.Content("~/"));
```

ログイン状態では`/Contact`ページを閲覧できます。

![キャプチャ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195024/1d1c5a41-cec3-b55d-efc7-8eaf1f62107f.png)