ASP.NET Core 有提供一些預設的 authentication scheme 可以使用,常見的有 Cookie Authentication 或是 JWT Bearer Token。當然,我們如果有其他需求也可以依照需要的效果自訂 authentication scheme 以滿足特殊規則的驗證方法。

要建立一個 authentication scheme 需要包含 scheme 的名字、AuthenticationHandler 以及 AuthenticationSchemeOptions。

假設我們需要一個 scheme,這個 scheme 的驗證方式為取得請求 Header 中鍵為 X-Foo 的值,使用該值與程式內的通關密語(bar)相比對,一樣的話就可以通過驗證。我們以這個情境自訂一個 FooBarAuthenticationHandler。

自訂 AuthenticationSchemeOptions

AuthenticationSchemeOptions 可以放入任何不想寫死在 AuthenticationHandler 裡面的設定,例如可以把 Header 的名稱及通關密語都定義在 AuthenticationSchemeOptions 中,稍後就可以將 Header 的名稱 X-Foo 及通關密語 bar 都由外部傳入。

public class FooBarAuthenticationOptions : AuthenticationSchemeOptions
{
    public string HeaderName { get; set; } = null!;
    public string Secret { get; set; } = null!;
}

AuthenticationHandler 的定義方式有兩種,一種是直接實作 IAuthenticationHandler 介面,另一種是繼承並實作AuthenticationHandler<TOptions>

直接實作介面的好處是可以少掉依賴AuthenticationHandler<TOptions>這個虛擬類別,但是缺點就是需要自己實作所有功能,沒辦法享用 AuthenticationHandler<TOptions> 虛擬類別為我們實作好的基本功能。 如果需要完全自訂義的話會建議使用 IAuthenticationHandler 介面自行實作所有方法,而如果是有使用 AuthenticationSchemeOptions 的需求或是需要預設實作的話,建議使用繼承 AuthenticationHandler<TOptions> 的方式自訂驗證邏輯。

自訂 AuthenticationHandler

這裡使用繼承 AuthenticationHandler<TOptions> 的方式自訂 AuthenticationHandler。

首先定義一個 FooBarAuthenticationHandler 類別,該類別繼承 AuthenticationHandler<FooBarAuthenticationOptions> 並且定義建構函式初始化該類別。

public class FooBarAuthenticationHandler : AuthenticationHandler<FooBarAuthenticationOptions>
{
    public FooBarAuthenticationHandler(IOptionsMonitor<FooBarAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder)
    {
		
    }
}

接著在我們自訂的 FooBarAuthenticationHandler 類別中加入 HandleAuthenticateAsync 函式,並且 override 父類別的 HandleAuthenticateAsync 方法,在使用此 handler 驗證時便會依照我們自訂義的驗證邏輯進行驗證。

protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
	// 檢查Options中的值是否存在
	if (string.IsNullOrEmpty(Options.HeaderName) || string.IsNullOrEmpty(Options.Secret))
	{
		throw new Exception("HeaderName and Secret are required for FooBarAuthenticationHandler.");
	}

	// 取得請求中特定Header的值
	if (!Context.Request.Headers.TryGetValue(Options.HeaderName, out var headerStringValues))
	{
		return AuthenticateResult.Fail("Missing header");
	}

	// 檢查Header的值是否與Options中的Secret相符
	if (headerStringValues.ToString() != Options.Secret)
	{
		return AuthenticateResult.Fail("Invalid secret");
	}

	var claims = new List<Claim>()
	{
		new Claim(GatewayAuthenticationDefaults.AuthenticationScheme, "Success")
	};
	var claimsIdentity = new ClaimsIdentity(claims, GatewayAuthenticationDefaults.AuthenticationScheme);
	var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

	return AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, GatewayAuthenticationDefaults.AuthenticationScheme));
}

HandleAuthenticateAsync 的返回型態是 AuthenticateResult,常見的方法有 Success、Fail 及 NoResult。Success 用來表示驗證成功,Fail 用來表示驗證失敗,NoResult 用來表示不成功也不失敗。

NoResult 稍微有點難以理解其作用,因為通常不是成功就是失敗,但查了一下發現有兩種情境可能會用到。第一種是假設有多個 AuthenticationHandler 可以使用,若想要讓某個 AuthenticationHandler 跳過驗證並且執行另一個驗證,這時就可以使用 NoResult。另一個情境是希望在特定條件下(例如以某個字串開頭的API)不執行某些驗證,也可以使用 NoResult 來達成此效果。

除了 HandleAuthenticateAsync 可以被 override 以外,HandleChallengeAsync 及 HandleForbiddenAsync 也可以被 override,可依照需求選擇要 override 的函式。

protected override Task HandleChallengeAsync(AuthenticationProperties? properties)
{
	Context.Response.StatusCode = 401;
	Context.Response.Headers.Append(
		"WWW-Authenticate",
		$"{Options.HeaderName} realm=\"foo\", error=\"invalid_secret\", error_description=\"Missing foo header\"");
	return Task.CompletedTask;
}

protected override Task HandleForbiddenAsync(AuthenticationProperties? properties)
{
	Context.Response.StatusCode = 403;
	return Task.CompletedTask;
}

到這裡我們已經定義了 AuthenticationSchemeOptions 和 AuthenticationHandler,接著要定義 authentication scheme 將這些流程包裝成一個完整的驗證方案。

自訂 AuthenticationScheme

在定義 authentication scheme 時會需要賦予 scheme 一個名稱,參考微軟預設的 scheme 時發現通常會用某個 const 的字串來定義 scheme 的名稱,屆時在使用[Authorize] attribute 時就可以直接使用該字串來指定要驗證的 scheme。

public static class FooBarAuthenticationDefaults
{
    public const string AuthenticationScheme = "FooBar";
}

AuthenticationBuilder 的 AddScheme 方法需要指定 AuthenticationSchemeOptions 及 AuthenticationHandler<TOptions> 的類別,這裡我們使用 FooBarAuthenticationOptions 及 FooBarAuthenticationHandler 符合其要求。另外,需要在參數中指定 scheme 的名字及 Action<FooBarAuthenticationOptions> 的 callback function,由於包裝這層的目的是便於在 Program.cs 中呼叫,我們將 Options 的 callback 直接由參數傳入,讓呼叫者可以直接設定 Options。

public static class AuthenticationBuilderExtension
{
    public static AuthenticationBuilder AddFooBar(this AuthenticationBuilder builder, Action<FooBarAuthenticationOptions> configureOptions)
        => builder.AddScheme<FooBarAuthenticationOptions, FooBarAuthenticationHandler>(FooBarAuthenticationDefaults.AuthenticationScheme, configureOptions);
}

將自訂的 AuthenticationScheme 加入到驗證服務中

在 Program.cs 中我們可以直接在 AddAuthentication() 後面加上我們自訂的 AddFooBar() 方法,並設定我們設計要傳入的設定值參數,在執行驗證時就會依照設定值進行驗證。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication()
    .AddFooBar(options =>
    {
        options.HeaderName = "X-Foo";
        options.Secret = "bar";
    })

指定 AuthenticationSchemes 進行驗證

在 AuthorizeAttribute 中的 AuthenticationScheme 指定特定的 scheme 即可以該 scheme 進行驗證。

[ApiController]
[Authorize(AuthenticationSchemes = FooBarAuthenticationDefaults.AuthenticationScheme)]
public class FooBarController : ControllerBase
{

}
參考資料