KENTEM TechBlog

建設業のDXを実現するKENTEMの技術ブログです。

【C#】Authorize 属性ってなに?Cookie 認証をベースに ASP.NET Core の認証・認可を理解しよう!

KENTEM でバックエンドを担当している N.Y です。

よく C# で書いた API Controller のメソッドに、 [Authorize] という属性(アトリビュートとも言う)が付いているのを見たことはありませんか??

[Authorize] // ←これ
public IActionResult Dashboard()
{
    // なにかしらの処理
    return View();
}

ASP.NET Core 側で用意されている属性で、認証・認可で使用されているっぽいな~ということしか知りませんでしたが C# エンジニアになるには理解は必須だと思い、今回調べてみました。

認証・認可について

既にご存じの方も多いかと思いますが、定義のおさらいです。

認証 (Authentication) とは

あなたは誰ですか?」を確認するプロセスです。
ユーザーが提供した情報(ユーザー名とパスワードなど)が正しいか検証し、
そのユーザーが本人であることを証明します。

認可 (Authorization) とは

あなたはこれを行うことができますか?」を確認するプロセスです。
認証されたユーザーが、特定の機能やデータへのアクセス権限を持っているかを判断します。

Authentication と Authorization ...字面が似すぎています。
何度も見ていると覚えられるのでしょうかね・・・。

Authorize 属性の役割

ASP.NET Core の Authorize 属性を設定すると、認証されていないユーザーをアクセスさせないといった制限が可能になります。 必要に応じて、認可の条件(ロールやポリシー)でさらに制限を加えることもできます。

例えば、次のような形で使用することができます。

認証済みかどうかを判別する

[Authorize]
public class DashboardController : Controller
{
    public IActionResult Dashboard()
    {
        return View();
    }
}

認証済みのユーザーのみアクセスすることができます。
API 単体にも付与できますし、コントローラー自体にも付与することが可能です。

指定されたロールを持つかを判別する

[Authorize(Roles = "Admin")]
public IActionResult AdminPanel()
{
    return View();
}

Admin のロールを持つユーザーのみアクセスすることができます。

指定されたポリシーを持つかを判別する

[Authorize(Policy = "CanDeleteData")]
public IActionResult Delete()
{
    // 削除処理
    return RedirectToAction("Index");
}

CanDeleteData のポリシーを持つユーザーのみアクセスすることができます。

認証・認可のベース部分を作ろう

ここまでで [Authorize] 属性がどのような役割を持っているのかは理解できましたが、[Authorize] だけ書けばあとは自動で・・・ということではもちろんありません。
認証・認可のベースを作成して初めて [Authorize] が正しく動作します。

ベース部分の作成には、大きく分けて次の2つのことが必要です。
1. Program.cs にサービスとミドルウェアを設定
2. ユーザー認証を実際に行うハンドラーの作成

今回はCookie 認証を使用した例で、どのようなベース部分を作成する必要があるのか深掘りしてみました。

留意点
※コードサンプルは、ASP.NET Core Web アプリ(Model-View-Controller)のテンプレートを利用しています。
※ASP.NET Core Identity を使用しない方法を採用しています。

Program.cs にサービスとミドルウェアを設定

Program.cs に、認証・認可に必要なサービスおよびミドルウェアの設定を行います。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();

// 認証サービスを追加
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = "CookieAuthentication"; // デフォルトの認証スキームをクッキー認証に設定
})
.AddCookie("CookieAuthentication", options => // "CookieAuthentication" という名前でクッキー認証を設定
{
    options.LoginPath = "/Account/Login"; // 未認証時にリダイレクトされるログインページのパス
    options.AccessDeniedPath = "/Account/AccessDenied"; // 権限がない場合にリダイレクトされるパス
    options.ExpireTimeSpan = TimeSpan.FromMinutes(30); // クッキーの有効期限
    options.SlidingExpiration = true; // 有効期限をスライドさせる(アクセスがあるたびに延長)
});

// 認可サービスを追加
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("CanDeleteData", policy => policy.RequireRole("Admin"));

var app = builder.Build();

~(省略)~

app.UseRouting();

// 認証ミドルウェアと認可ミドルウェアを有効化 (順序が重要!)
app.UseAuthentication(); // これが認証クッキーを読み取り、HttpContext.Userを設定する
app.UseAuthorization();  // これが[Authorize]属性を評価し、アクセスを制御する

認証サービスを追加

builder.Services.AddAuthentication(options => { ... })

これは、ASP.NET Core のDI(依存性注入)コンテナに認証サービス全般を登録するためのメソッドです。
このメソッドを呼び出さないと、たとえ [Authorize] を使用しても認証機能が動作しません。

options.DefaultScheme = "CookieAuthentication";

デフォルトの認証方法を指定します。
[Authorize] のみの記述の場合、このデフォルト認証が使用されます。
ここで設定した "CookieAuthentication" は認証スキームと呼ばれ、アプリケーション内に複数の認証方法を設定する場合の一意な識別子となります。

CookieAuthenticationDefaults.AuthenticationScheme というクッキー認証のデフォルトスキーマ名もあるので、普通はそちらを使用するようです。
今回は分かりやすさ重視のため "CookieAuthentication" というカスタムスキーマ名で進めます。

.AddCookie("CookieAuthentication", options => { ... })

この中で、"CookieAuthentication" がどのようなクッキー認証なのかを設定しています。
options.LoginPath や、options.AccessDeniedPath を設定しておくことで、未ログインのユーザーや権限のないユーザーを自動的にページへリダイレクトすることができます。
また、options.ExpireTimeSpan などでログイン状態の維持時間を設定するなど、クッキー認証の様々な設定を行うことができます。

認可サービスを追加

builder.Services.AddAuthorizationBuilder()

これは、ASP.NET Core のDI(依存性注入)コンテナに認可サービス全般を登録するためのメソッドです。
[Authorize(Roles = "Admin")] のようにロールやポリシーを使用した認可のチェックを行いたい場合は、この記述は必要です。

.AddPolicy("CanDeleteData", policy => policy.RequireRole("Admin"));

明示的にポリシーを定義しています。
[Authorize(Policy = "CanDeleteData")] のようにポリシーによる認可のチェックを行いたい場合は、この記述が必要です。

認証ミドルウェアと認可ミドルウェアを有効化

app.UseAuthentication();

認証のミドルウェアを有効化します。
このミドルウェアはリクエストに含まれる認証情報(例:クッキーの内容など)を読み取り、それが信頼できるものか(有効かどうか)を検証します。
検証後は、認証情報からユーザーの身元 ClaimPrincipal(クレームを含む) を再構築し HttpContext.User プロパティに設定します。

app.UseAuthorization();

認可のミドルウェアを有効化します。
[Authorize] 属性が付いているかどうかを確認し、付いている場合には HttpContext.User に設定されている IsAuthenticated や Claim(クレーム)情報から、ロールやポリシーの要件を満たしているかを評価します。

この設定は必ず app.UseAuthentication() の後に記述する必要があります!!
app.UseAuthentication() が正しく HttpContext.User プロパティを設定した後でなければ、認可のチェックができないためです。

  • 参考 ミドルウェアの順序

learn.microsoft.com

これらのミドルウェアは、ユーザーがログインしているかどうかにかかわらず、すべてのHTTPリクエストに対して実行されます。
ユーザーがログインしていない場合は、匿名ユーザーとして扱われ [Authorize] 属性が付いている処理にはアクセスできません。

用語集

Claim(クレーム)

  • 認証されたユーザーに紐づく属性情報(例:名前、メールアドレス、ロールなど)
  • 一つ一つが「証明書に書かれた情報」のようなもの

ClaimsIdentity

  • Claim の集合 + 認証スキーム名
    認証スキーム名とは、どの認証方法で作成されたかを示す名前です。
  • 1人の「身元情報」を表すオブジェクト(=免許証に近い)

ClaimsPrincipal

  • 現在ログインしているユーザー全体を表す
  • 通常は 1 つの ClaimsIdentity を内包
  • HttpContext.User からアクセスされる

ユーザー認証を実際に行うハンドラーの作成

ASP.NET Core Identity を使用しない場合は、ユーザーの身元確認は自分で行う必要があります。

    [HttpPost]
    [ValidateAntiForgeryToken] // CSRF対策
    public async Task<IActionResult> Login(string username, string password, bool rememberMe, string returnUrl = null)
    {
        ViewData["ReturnUrl"] = returnUrl;

        // **1. ユーザーの身元確認(ユーザー名とパスワードの照合)**
        if (username == "testuser" && password == "password123")
        {
            // **2. ユーザーの身元が確認された後、そのユーザーに関するクレームを定義**
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.NameIdentifier, username),
                new Claim(ClaimTypes.Name, "テストユーザー"),
                new Claim(ClaimTypes.Role, "User"),
                new Claim(ClaimTypes.Role, "Admin") // 複数のロールも可能
            };

            // **3. 定義されたクレームから ClaimsIdentity を構築**
            //    ユーザーがどのように認証されたか(認証スキーム)を示す
            var claimsIdentity = new ClaimsIdentity(claims, "CookieAuthentication");

            // **4. ClaimsIdentity から ClaimsPrincipal オブジェクトを構築**
            var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

            // **5. HttpContext.SignInAsync() を呼び出して認証Cookieを発行し、ユーザーをログインさせる**
            //    構築した ClaimsPrincipal を渡し、認証スキーム(クッキー認証)を指定する
            await HttpContext.SignInAsync(
                "CookieAuthentication",
                claimsPrincipal,
                new AuthenticationProperties
                {
                    IsPersistent = rememberMe, // true にするとセッションを超えてログイン状態が保持される
                });

            // ログイン成功後のリダイレクト先
            if (Url.IsLocalUrl(returnUrl))
            {
                return Redirect(returnUrl);
            }
            else
            {
                return RedirectToAction("Index", "Home");
            }
        }

        // ログイン失敗
        return View();
    }

ここでのポイントは以下2つです。
1. 認証されたユーザーのクレーム情報を作成する
2. await HttpContext.SignInAsync(...) で認証スキームのハンドラーを自動で実行

認証されたユーザーのクレーム情報を作成する

var claims = new List<Claim>...

new Claim("キー", "値") で、認証したユーザーの名前やメールアドレス、ロールやポリシーなどを作成します。
今回は簡単に固定値を設定していますが、実務ではユーザーごとにロールやポリシーは判定され設定する必要があります。

作成したクレーム情報を元に ClaimsIdentity ClaimsPrincipal を作成します。

認証スキームのハンドラーを自動で実行

await HttpContext.SignInAsync(...)

ASP.NET Core の認証システムが提供しているメソッドです。
内部では指定された認証スキームのハンドラーが起動し、ClaimPrincipal を使用した認証 Cookie が発行されます。
発行されたクッキーはレスポンスヘッダーの Set-Cookie に追加されます。

また、 HttpContext.User プロパティに最新の ClaimPrincipal が設定されます。
これにより、この後のコードでは User.Identity.IsAuthenticated が true になりユーザーのクレーム情報にアクセスが可能になります。
例)User.Identity.Name など

流れのまとめ

ここまでの流れをざっくり図にまとめてみました。

サンプルコードをすべて貼ると長くなってしまいそうでしたので、ログインページなどの機能のコードについては割愛しています。

興味のある方は是非ログインページからログイン機能、ログアウト機能まで作成してみてください!

今回は↓こんな感じのものを作成しました。

所感

今回は、ずっと疑問に思っていた [Authorize] 属性について深掘りしてみました。

ASP.NET Core が頑張ってくれる部分は普段はあまり気にしなくて良いと思うのですが、気になった時にはどのような内部構造になっているのか時間をじっくり取ってみるのも勉強になりますね。
「私、、、気になります!!」の精神は結構大事だと思っているので、今後もニッチな技術深掘りをしていきたいものです。

おわりに

KENTEMでは、様々な拠点でエンジニアを大募集しています! 建設×ITにご興味頂いた方は、是非下記のリンクからご応募ください。 recruit.kentem.jp career.kentem.jp