.NET(5) MVCでCookieによる認証


.NETで認証と言えばIdentityですが、まずは基本となるCookieによる認証（保持）を。

## やりたいこと（仕様概要）

MVCの標準テンプレートが生成する画面のPrivacyエリアに認証機能をつける。
（ログイン画面は無いので新規に作ります）

![スクリーンショット 2021-01-30 14.21.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/c68928e8-223e-ba7d-b839-bda787997a92.png "スクリーンショット 2021-01-30 14.21.55.png")


## 準備

mvcの雛形を作成

```bash
dotnet new mvc -o dotnet_auth_basic
cd dotnet_auth_basic
```

## 実装

### 大まかな流れ

凡そ下記のような流れ。

* Startup.csのConfigureServices()でCookieを利用と条件を設定
* Startup.csのConfigure()でapp.UseAuthenticationして有効化
* ログイン画面とログイン処理用のControllerを実装
* ログイン画面を作成（ログアウトボタン等も作成）
* 認証が必要な箇所に[Authorize]を適用
* 実行


### Startup.cs

では、Startup.csから。ConfigureServices()内でCookieの利用を設定。Configure()内で有効化。

```csharp:Startup.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

//追加
using Microsoft.AspNetCore.Authentication.Cookies;

namespace dotnet_auth_basic
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();

            //追加
            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
                {
                    options.AccessDeniedPath = "/Account/Login";
                    options.LoginPath = "/Account/Login";
                });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }
            // app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            //追加
            app.UseAuthentication();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}
```

### 認証データの受け渡しにつかうモデル作成

コントローラー作成の前に、その中でつかうクラスを作成しておきます。
めんどくさいのでプロジェクト直下に下記クラスを作っておきます。


```csharp:ApplicationUser.cs
using System;

namespace dotnet_auth_basic
{
    public class ApplicationUser
    {
        public string UserName { get; set; }
        public string Password { get; set; }

        public string ReturnRul { get; set;}

        public ApplicationUser() { }

    }
}
```

### AccountController.cs（新規）

Controllerフォルダ内に新規にファイルを作成し下記を実装。
Get用メソッドは、ログイン画面の表示、POST用メソッドはCookieを発行（簡易認証後）。


```csharp:AccountController.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using dotnet_auth_basic.Models;

//追加
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;

namespace dotnet_auth_basic.Controllers
{
    public class AccountController : Controller
    {
        //login(get)
        [HttpGet]
        public IActionResult Login(string returnUrl = null)
        {
            // Console.WriteLine(returnUrl);
            ViewData["returnUrl"] = returnUrl;
            return View();
        }

        //login post
        [HttpPost]
        [AutoValidateAntiforgeryToken]
        public async Task<IActionResult> Login(ApplicationUser user, string returnUrl = null)
        {

            //値取得
            var username = user.UserName;
            var password = user.Password;

            //パスワードが一致しない
            if (username == "hoge" && password == "password")
            {

                //claimsIdentityを作成
                var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
                identity.AddClaim(new Claim(ClaimTypes.Name, username));

                //暗号化cookie作成しサインイン
                await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));

                if (!string.IsNullOrEmpty(returnUrl))
                {
                    //要認証ページからのリダイレクト等の場合
                    return Redirect(returnUrl);
                }
                else
                {
                    //loginにダイレクトにアクセスがあったときなど
                    return RedirectToAction(nameof(HomeController.Index), "Home");
                }
            }
            else
            {
                //ID,PWエラー
                return RedirectToAction("Login", "Account");
            }
        }

        //logout
        public async Task<IActionResult> Logout()
        {
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            return RedirectToAction("Index", "Home");
        }
    }
}
```

### Login.cshtml（新規）

ログイン画面を実装。@ModelでreturnUrlが取れなかったのでViewData()を利用している。

```html:Views/Account/Login.cshtml
@model ApplicationUser
@{
    <form asp-antiforgery="true" asp-controller="Account" asp-action="Login">
        User name: <input name="username" type="text" />
        Password: <input name="password" type="password" />
        <input name="submit" value="Login" type="submit" />
        <input type="hidden" name="returnUrl" value=@ViewData["returnUrl"] />
    </form>
}
```

### _Layout.cshtml（既存：抜粋）

_Layout.cshtmlを編集し、ヘッダ部分にlogin, logoutボタンを表示。

```html:Views/Shared/_Layout.cshtml（抜粋）
　　　　　　　　　<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home"
                                asp-action="Privacy">Privacy</a>
                        </li>
                    </ul>
                    <ul class="nav navbar-nav navbar-right">
                        @if (User.Identity.IsAuthenticated)
                        {
                            <li><a asp-area="" asp-controller="Account" asp-action="Logout">Logout</a></li>
                        }
                        else
                        {
                            <li><a asp-area="" asp-controller="Account" asp-action="Login">Login</a></li>
                        }
                    </ul>
                </div>
```

### HomeController.cs

HomeControllerのPrivateは認証必要エリアとする。

```csharp:HomeController.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using dotnet_auth_basic.Models;

//追加
using Microsoft.AspNetCore.Authorization;

namespace dotnet_auth_basic.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

        public HomeController(ILogger<HomeController> logger)
        {
            _logger = logger;
        }

        public IActionResult Index()
        {
            return View();
        }
        
        [Authorize] //追加
        public IActionResult Privacy()
        {
            return View();
        }

        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
}
```

## 動作確認

```bash
dotnet run
```