プログラムを中心とした個人的なメモ用のブログです。 タイトルは迷走中。
内容の保証はできませんのであしからずご了承ください。

2017/09/23

ASP.NET Core Identity を使わない認証

update2017/11/07 event_note2017/09/23 5:09

ASP.NET Core において、Identity を使わない認証はどうやってやるのか調べてみました。
ただし、ASP.NET Core 2.0 以降とそれより前とで大きく異なるようです。

ここでは ASP.NET Core 2.0 以降を対象とします。
基本的に Microsoft のページに書いてあるのですが、その通りにやっても全く動きませんでした。
というかいろいろ説明不足だなと感じます(もしくは前提となる知識が私に不足しているか)。

そんなわけで、上記のページ書いてあることの要約+補足事項みたいな感じになっています。

環境

  • Visual Studio 2017
  • ASP.NET Core 2.0

プロジェクトの作成

ASP.NET Core 2.0 のテンプレートプロジェクトをベースにして説明したいと思うので、ASP.NET Core でプロジェクトを作成します。
認証はなしにしておきます。

準備

Startup.csConfigure メソッドに UseAuthentication を追加します。
尚、これは UseMvc より先に記述する必要があります(これで結構はまった・・・)。

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // ...

    app.UseAuthentication();

    // ...
}

続いて、ConfigureServices メソッドに AddAuthenticationAddCookie を追加します。
これは別に AddMvc より先に記述しなくても動作しました。

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

    services.AddAuthentication("MyCookieAuthenticationScheme")
        .AddCookie("MyCookieAuthenticationScheme", options =>
        {
            options.AccessDeniedPath = "/Account/Forbidden/";
            options.LoginPath = "/Account/Unauthorized/";
        });
}

デフォルトのスキーマ名を使用する場合は以下のように指定します。

using Microsoft.AspNetCore.Authentication.Cookies;

//...

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
    options.AccessDeniedPath = "/Account/Forbidden/";
    options.LoginPath = "/Account/Unauthorized/";
});
  • AccessDeniedPath はアクセスが禁止されているリソースにアクセスしようとしたときにリダイレクトする相対パスです。
  • LoginPath は認証されていないユーザーがリソースにアクセスしようとしたときにリダイレクトする相対パスです。

ユーザークラスの作成

Controller を実装する前に、先に認証するユーザーを登録するためのクラス作成します。
プロジェクト直下に以下のようなクラス作成します。

namespace プロジェクト名
{
    public class ApplicationUser
    {
        public string UserName { get; set; }
        public string Password { get; set; }

        public ApplicationUser() { }
    }
}

Controller の実装

認証を行うための AccountController を作成します。
ここで認証を行うユーザーも定義しておきます。
※ここではコード上で直接定義していますが、実際にはもちろんこんなことはしません。

public class AccountController : Controller
{
    List<ApplicationUser> users = new List<ApplicationUser> {
        new ApplicationUser{UserName = "hoge", Password = "1234"},
        new ApplicationUser{UserName = "piyo", Password = "5678"}
    };

    // ...
}

ログイン処理

AccountController に以下のようなアクションメソッドを追加します。

using System.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authentication;

// ...

public IActionResult Login()
{
    return View();
}

[HttpPost]
[AutoValidateAntiforgeryToken]
public async Task<IActionResult> Login(ApplicationUser user, string returnUrl = null)
{
    const string badUserNameOrPasswordMessage = "Username or password is incorrect.";
    if (user == null)
    {
        return BadRequest(badUserNameOrPasswordMessage);
    }

    // ユーザー名が一致するユーザーを抽出
    var lookupUser = users.Where(u => u.UserName == user.UserName).FirstOrDefault();
    if (lookupUser == null)
    {
        return BadRequest(badUserNameOrPasswordMessage);
    }

    // パスワードの比較
    if (lookupUser?.Password != user.Password)
    {
        return BadRequest(badUserNameOrPasswordMessage);
    }

    // Cookies 認証スキームで新しい ClaimsIdentity を作成し、ユーザー名を追加します。
    var identity = new ClaimsIdentity("MyCookieAuthenticationScheme");
    identity.AddClaim(new Claim(ClaimTypes.Name, lookupUser.UserName));

    // クッキー認証スキームと、上の数行で作成されたIDから作成された新しい ClaimsPrincipal を渡します。
    await HttpContext.SignInAsync("MyCookieAuthenticationScheme", new ClaimsPrincipal(identity));

    return RedirectToAction(nameof(HomeController.Index), "Home");
}

Login というアクションメソッドが2つあります。

何も属性を付与していないほうは、/Account/Login/ にアクセスしたときの GET メソッドに対するアクションメソッドになります。

[HttpPost] 属性が付与されているほうは、その名の通り POST メソッドのときのアクションメソッドになります。
また、[AutoValidateAntiforgeryToken] 属性はクロスサイトリクエストフォージェリを防止するために付与する属性です。

POST に対するアクションメソッドの中身についてですが、前半はユーザー情報の比較を行っているだけです。

認証したらクッキーを作成しますが、クッキーを作成するには、クッキーにシリアライズした情報を保持するための ClaimsPrincipal を作成する必要があるみたいです。
それが以下のコードです。

var identity = new ClaimsIdentity("MyCookieAuthenticationScheme");
identity.AddClaim(new Claim(ClaimTypes.Name, lookupUser.UserName));

適切な ClaimsPrincipal オブジェクトを取得したら、Controller メソッド内で以下のように呼び出します。

await HttpContext.SignInAsync("MyCookieAuthenticationScheme", principal);

これにより、暗号化されたクッキーが作成され、現在のレスポンスに追加されます。 SignInAsync を呼び出すときは、設定時に指定したスキーマ名を使用する必要があります。

ログアウト処理

ログアウトは簡単で、以下のように記述するだけです。

public async Task<IActionResult> Logout()
{
    await HttpContext.SignOutAsync("MyCookieAuthenticationScheme");

    return RedirectToAction("Index", "Home");
}

View の実装

Controller を実装したら、ユーザーが ID とパスワードを入力するためのフォームを作成する必要があります。

View の AccountLogin.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="@Model" />
    </form>
}

入力した値は、name 属性と一致するアクションメソッドのパラメーターにバインディングされます。
尚、大文字小文字の違いがあってもきちんとバインディングされるようです。

あとは、ログインとログアウトのためのリンクを全ページに表示するため、_Layout.cshtml に以下のコードを追加します。

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

例えば、以下のようにヘッダーの右側に追加します。

<div class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
        <li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
        <li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
        <li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</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>

参考 URL