客製 CSRF/XSRF Token 驗證 for NET8 React and ASP.NET Core 專案

引言

經幾天試用 .NET8 的 React and ASP.NET Core 專案,預設的 Anti-Forgery 機制並不合用(上一版的可以)。思考後決定自製。未來若又可用了就用回去預設的 Anti-Forgery 機制。

自訂 Anti-forgery 機制

用於登入。流程大蓋如下圖。

1) 當一進入 Login page 時,自動抓取 XSRF token 回來。

2) 送出登入封包時,夾帶 XSRF token 上去檢查,若登入成功送回 Auth Cookie。

其中,不管是 XSRF toekn 還是 Auth Cookie 都用 cookie 送回,並加上同源政策檢查,httpOnly=true, Secure=true, SameSite=Lax。

程式碼紀錄

這裡只看 XSRF token 的部份。

開發成 Filter 來使用更方便。

AccountController.cs
[AllowAnonymous]
[ApiController]
[Route("api/[controller]")]
public class AccountController(ILogger<AccountController> _logger, IMemoryCache _cache, AccountService _account) : ControllerBase
{
  [HttpPost("[action]")]
  public ActionResult<string> GetXsrfToken()
  {
    ValidateXsrfTokenFilter.ResponseAndStoreXsrfToken(this.HttpContext, _cache);
    return NoContent();
  }

  [ServiceFilter<ValidateXsrfTokenFilter>]
  [HttpPost("[action]")]
  public async Task<ActionResult> Login(LoginArgs login)
  {
    [...略...]
  }
}

XSRF 相關的碼集中在一起。開發成 Filter 來使用更方便。

ValidateXsrfTokenFilter.cs
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Caching.Memory;
using Vista.Models;

namespace YourProject.Server.Models;

/// <summary>
/// for Anit-Forgery
/// </summary>
public class ValidateXsrfTokenFilter(ILogger<ValidateXsrfTokenFilter> _logger, IMemoryCache _cache) : Attribute, IAuthorizationFilter
{
  const string XSRF_TOKEN_NAME = ".Your.XSRF-TOKEN-xyz";

  public void OnAuthorization(AuthorizationFilterContext context)
  {
    try
    {
      if (!context.HttpContext.Request.Cookies.TryGetValue(XSRF_TOKEN_NAME, out string? extractedToken))
      {
        _logger.LogError("XSRF-TOKEN is missing.");
        context.Result = new UnauthorizedResult();
        return;
      }

      Guid loginSid = Utils.AesSimpleDecrypt<Guid>(extractedToken);
      if (!_cache.TryGetValue<string>($"XSRF-TOKEN:{loginSid}", out string? _token))
      {
        _logger.LogError("XSRF-TOKEN is not exists.");
        context.Result = new UnauthorizedResult();
        return;
      }

      // 取出 token 後就可移除。確保只能用一次。
      _cache.Remove($"XSRF-TOKEN:{loginSid}");

      if (_token != extractedToken)
      {
        _logger.LogError("XSRF-TOKEN is invalid.");
        context.Result = new UnauthorizedResult();
        return;
      }

      // 送回新的 XSRF-TOKEN
      ResponseAndStoreXsrfToken(context.HttpContext, _cache);
    }
    catch(Exception ex)
    {
      _logger.LogError(ex, "XSRF-TOKEN exception.");
      context.Result = new UnauthorizedResult();
    }
  }

  /// <summary>
  /// Procedure
  /// </summary>
  public static void ResponseAndStoreXsrfToken(HttpContext context, IMemoryCache cache)
  {
    Guid loginSid = Guid.NewGuid();
    string token = Utils.AesSimpleEncrypt(loginSid);
    /// 此 anit-forgery-token 只能做到不能重複 post 同一個封包。 
    /// 正式版的 anit-forgery-token 檢驗依據項目應加入 client side 一些識別資訊!
    cache.Set($"XSRF-TOKEN:{loginSid}", token, TimeSpan.FromMinutes(3)); // 3分鐘內需完成登入

    // 送回 cookie
    context.Response.Cookies.Append(XSRF_TOKEN_NAME, token, new CookieOptions()
    {
      Expires = DateTimeOffset.Now.AddMinutes(3), 
      SameSite = SameSiteMode.Lax, // SameSiteMode.Strict,
      Secure = true,
      HttpOnly = true,
      IsEssential = true, // for GDPR Consent. 若該 cookie 為 essential 就不需使用者同意就可寫入。.
    });
  }
}

別忘了註冊

Program.cs
var builder = WebApplication.CreateBuilder(args);

//§ for Anit-Forgery
builder.Services.AddScoped<ValidateXsrfTokenFilter>();

...略...

完整程式碼

(EOF)

Last updated