2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

BlazorでシンプルにCookie認証 (.Net 8)

Posted at

1.この記事の目的

2.前提条件

  • Visual studio 2022 Version 17.12.2
  • .Net 8
  • UI:Blazor Web App
  • Blazor rendermode: Static Server + InteractiveServerRenderMode
  • WebAPIを利用しない
  • 認証・認可にはCookieを利用
  • 認証ロジックはここでは単純化し、あえてデータベースを利用しない

3.ソリューションのソース

4.求めている挙動

4.1 ログインの流れ

image.png

ログイン成功

  • Cookieが作成される
  • Blazorのフォーム上でユーザー名や権限(ロール)を取得できる

image.png

4.2 ログインユーザーのみ表示できるページの挙動

image.png

weatherページには @attribute [Authorize]を指定してる。

@page "/weather"
@attribute [Authorize]

ログアウトした状態(Cookieが存在しない)で
image.png

https://localhost:7022/weatherをGETすると
ログインページへリダイレクトされる

image.png

5.認証方法

5.1 利用する認証スキーム

5.2 SignInAsyncを呼び出す認証サービスの作成

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

namespace BlazorCookie.Services
{
    public class AuthenticationService
    {
        private readonly IHttpContextAccessor _httpContextAccessor;

        public AuthenticationService(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
        }

        public async Task<bool> LoginAsync(LoginUser loginUser)
        {

            var claims = new List<Claim>();

            // 本来の認証判定はもっと複雑だが、ここではテスト用にシンプルに認証
            if (loginUser.UserName == "admin@test.com" && loginUser.Password == "test")
            {
                claims.Add(new Claim(ClaimTypes.Name, loginUser.UserName));
                claims.Add(new Claim(ClaimTypes.Role, "Administrator"));
            }
            else if (loginUser.UserName == "user@test.com" && loginUser.Password == "test")
            {
                claims.Add(new Claim(ClaimTypes.Name, loginUser.UserName));
                claims.Add(new Claim(ClaimTypes.Role, "User"));
            }
            else
            {
                return false;
            }

            var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);

            var httpContext = _httpContextAccessor.HttpContext;
            if (httpContext == null)
                throw new InvalidOperationException("HttpContext is not available.");

            await httpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                new ClaimsPrincipal(identity));
            return true;

        }
        public async Task LogoutAsync()
        {
            var httpContext = _httpContextAccessor.HttpContext;
            if (httpContext == null)
                throw new InvalidOperationException("HttpContext is not available.");

            await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }
    public class LoginUser
    {
        public string UserName { get; set; } = "";
        public string Password { get; set; } = "";
    }

}

5.3 ログインページ

@page "/login"
@inject NavigationManager NavigationManager
@inject AuthenticationService AuthService
@attribute [AllowAnonymous]
@* rendermodeは Static Server 固定 *@
<h3>Login</h3>

<p>admin@test.com / test</p>
<p>user@test.com / test</p>

<EditForm Model="@loginUser" FormName="login-form" OnValidSubmit="LoginAsync">
    <div>
        <div class="mb-3">
            <label class="form-label">ユーザー名</label>
            <InputText @bind-Value="loginUser.UserName" class="form-control"></InputText>
        </div>
        <div class="mb-3">
            <label class="form-label">パスワード</label>
            <InputText @bind-Value="loginUser.Password" class="form-control"></InputText>
        </div>
    </div>
    <button type="submit" class="btn btn-primary">ログイン</button>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private LoginUser loginUser { get; set; } = new();

    private async Task LoginAsync()
    {
        var success = await AuthService.LoginAsync(loginUser);
        if (success)
        {
            NavigationManager.NavigateTo("", forceLoad: true);
        }
    }

}

5.4 ログアウトページ

@page "/logout"
@inject AuthenticationService AuthService
@inject NavigationManager NavigationManager

@code {
    protected override async Task OnInitializedAsync()
    {
        await AuthService.LogoutAsync();

        NavigationManager.NavigateTo("", forceLoad: true);
    }
}

5.5 認証状態を判定するサンプル(Homeページ)

@page "/"
@attribute [AllowAnonymous]

<PageTitle>Home</PageTitle>

<h1>Home</h1>

<div class="mb-3">
    <AuthorizeView Roles="Administrator">
        <div class="mb-3">
            あなたのロールは "Administrator" です。
        </div>
    </AuthorizeView>
    <AuthorizeView Roles="User">
        <div class="mb-3">
            あなたのロールは "User" です。
        </div>
    </AuthorizeView>
    
    <AuthorizeView>
        <Authorized>
            <p>ログイン済み: @context.User.Identity!.Name</p>
            <a href="/logout">LogOut</a>
        </Authorized>
        <NotAuthorized>
            <div class="mb-3">
                <p>ログインしてください。</p>
                <a href="/login">Login</a>
            </div>

            <div class="mb-3">
                <p>テスト用:InteractiveServerのフォームではEXCEPTIONが発生する</p>
                <a href="/login-interactive">Login - InteracticeServer</a>
            </div>
        </NotAuthorized>
    </AuthorizeView>
</div>

6.注意点

  • Loginページの rendermode は Static Server 一択
  • InteractiveServer や WebAssermbly は利用できない

色々試して納得できたので共有します。

6.1 まずはStatic ServerでLoginした場合の挙動

(1) loginページにユーザー名+パスワードを入力しPOSTする
(2) POSTの応答ヘッダーに
  set-cookie:.AspNetCore.Cookies=CfDJ8ODTl-is4Y5Im-eN4-9_d.....がある。

image.png

(3) ログイン成功後、homeへのリダイレクト要求ヘッダーには当然このCookieが含まれる
image.png

ここまでの挙動は目的通り

6.2 InteraciveServerでLoginした場合の挙動

(1) loginページにユーザー名+パスワードを入力しSUBMITしても、POSTは発生しない
( InteractiveServer であれば SignalRのバイナリ通信だけが発生する )
image.png

(2) ログを見るとエラーが発生している
image.png

(3) エラーメッセージを要約すると

  • ログインページのGETはすでにレスポンスした
  • ログインボタンのSUBMITはSignalRでの通信でPOSTやGETではない
  • SignalRの通信内で、GETの応答ヘッダーに含めるCookieを発行するのはダメ!

言われてみればその通りなので納得です。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?