[NSwag] Swagger UI + Basic Authentication 訪問受保護的 Web API

ASP.NET Core 預設似乎沒有提供 Basic Authentication 的 DI,但仍然可以自行實作 AuthenticationHandler

ASP.NET Core 3.1

開發環境

  • VS 2019
  • ASP.NET Core 3.1
  • NSwag.AspNetCore.13.2.5

 

實作

BasicAuthenticationProvider.cs

驗證帳號密碼

這裡我建立一個假的帳號資訊,然後比對它們

public class BasicAuthenticationProvider : IBasicAuthenticationProvider
{
    //模擬db存放的資料
    private readonly List<User> _fakeUsers = new List<User>
    {
        new User
        {
            Id = 1, FirstName = "小章", LastName = "余", UserId = "yao", Password = "123456"
        }
    };
 
    public async Task<bool> Authenticate([NotNull] string userId, [NotNull] string password)
    {
        return this._fakeUsers
                   .Where(p => string.Compare(p.UserId, userId, true) == 0)
                   .Where(p => p.Password                             == password)
                   .Any();
    }
}

 

BasicAuthenticationHandler.cs

Basic Authententication

客戶端的請求要帶有 Authorization Header,Servererver 這裡要去做檢查,範例如下

Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

檢查 Header:

 

把用戶端請求的 Parameter 轉成字串,分割,取出帳號密碼:

 

驗證失敗彈跳對話視窗

 

完整代碼如下

public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
	private readonly IBasicAuthenticationProvider _authenticationProvider;

	public BasicAuthenticationHandler(
		IOptionsMonitor<AuthenticationSchemeOptions> options,
		ILoggerFactory                               logger,
		UrlEncoder                                   encoder,
		ISystemClock                                 clock,
		IBasicAuthenticationProvider                 authenticationProvider)
		: base(options, logger, encoder, clock)
	{
		this._authenticationProvider = authenticationProvider;
	}

	protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
	{
		if (!this.Request.Headers.ContainsKey("Authorization"))
		{
			return AuthenticateResult.Fail("Missing Authorization Header");
		}

		//檢查 Header
		//將 Authorization Header 轉型成 AuthenticationHeaderValue 物件
		AuthenticationHeaderValue.TryParse(this.Request.Headers["Authorization"], out var authenticationHeader);
		if (authenticationHeader == null)
		{
			return AuthenticateResult.Fail("Invalid Authorization Header");
		}

		//只允許 Basic Authentication
		if (string.Compare(authenticationHeader.Scheme, "basic", true) == 0 == false)
		{
			return AuthenticateResult.Fail("Only Support Basic Authority Header");
		}

		string userId   = null;
		string password = null;
		try
		{
			//Base64 String 轉成文字,切割,取出帳號密碼
			var credentialBytes = Convert.FromBase64String(authenticationHeader.Parameter);
			var credentials     = Encoding.UTF8.GetString(credentialBytes).Split(new[] {':'}, 2);
			userId   = credentials[0];
			password = credentials[1];
			var isValid = await this._authenticationProvider.Authenticate(userId, password);
			if (!isValid)
			{
				return AuthenticateResult.Fail("Invalid Username or Password");
			}
		}
		catch (Exception)
		{
			return AuthenticateResult.Fail("Invalid Authority Header");
		}

		//建立Claim,若需要更多資訊可以從資料庫拿
		var claims = new[]
		{
			//new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
			new Claim(ClaimTypes.Name, userId)
		};
		var identity  = new ClaimsIdentity(claims, this.Scheme.Name);
		var principal = new ClaimsPrincipal(identity);
		var ticket    = new AuthenticationTicket(principal, this.Scheme.Name);

		return AuthenticateResult.Success(ticket);
	}

	protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
	{
		this.Response.Headers["WWW-Authenticate"] = $"Basic realm=\"Demo APP\", charset=\"UTF-8\"";
		await base.HandleChallengeAsync(properties);
	}
}

 

Startup.cs

這裡要注意順序!(不要像我一樣)

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
 
    app.UseHttpsRedirection();
    app.UseStaticFiles();
 
    app.UseRouting();
    app.UseCors();
 
    app.UseAuthentication();
    app.UseAuthorization();
 
    app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
 
    // Add OpenAPI/Swagger middlewares
    app.UseOpenApi();    // Serves the registered OpenAPI/Swagger documents by default on `/swagger/{documentName}/swagger.json`
    app.UseSwaggerUi3(); // Serves the Swagger UI 3 web ui to view the OpenAPI/Swagger documents by default on `/swagger`
}

 

加入自訂驗證 BasicAuthenticationHandler

 

注入 BasicAuthenticationProvider 給 BasicAuthenticationHandler

 

設定 NSwag,送出 Authorization Header,Security Key 和 Processor Name 要一樣

 

完整代碼如下:

public void ConfigureServices(IServiceCollection services)
{
    services.AddCors();
    services.AddControllers();
 
    // configure basic authentication
    services.AddAuthentication("Basic")
            .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("Basic", null);
 
    // configure DI for application services
    //services.AddScoped<IBasicAuthenticationProvider, BasicAuthenticationProvider>();
    services.AddTransient<IBasicAuthenticationProvider, BasicAuthenticationProvider>();
 
    // Add OpenAPI v3 document
    //services.AddOpenApiDocument();
 
    services.AddOpenApiDocument(config =>
                                {
                                    var apiScheme = new OpenApiSecurityScheme
                                    {
                                        Type = OpenApiSecuritySchemeType.Basic,
                                        Name = "Authorization",
                                        In   = OpenApiSecurityApiKeyLocation.Header
 
                                        //Description = "Basic U3dhZ2dlcjpUZXN0"
                                    };
 
                                    config.AddSecurity("Basic", Enumerable.Empty<string>(),
                                                       apiScheme);
 
                                    config.OperationProcessors
                                          .Add(new AspNetCoreOperationSecurityScopeProcessor("Basic"));
                                });
 
    // Add Swagger v2 document
    // services.AddSwaggerDocument();
}

 

在 Controller 加上 AuthorizeAttribute

 

訪問 /swagger ,右上角有一個 Authorize 的按鈕,按下去,就會彈跳帳密對話視窗,這個時候還不會跑驗證

按下 Execute 時才會跑驗證

如此一來就能從 Swagger UI 送出 Authorization Header 了

 

換成 Postman 的效果也是一樣

 

擴充 Basic Authentication 的 Dependency Injection

先看結果,最終的結果如下圖,原本要注入的內容變少了,通通移到 AddBasic 擴充方法

有興趣的可以參考以下連結做法

https://joonasw.net/view/creating-auth-scheme-in-aspnet-core-2

 

範例位置

https://github.com/yaochangyu/sample.dotblog/tree/master/WebAPI/NSwag/Lab.DocAuth

 

參考

https://stackoverflow.com/questions/58363002/custom-authenticationhandler-not-working-in-asp-net-core-3

https://joonasw.net/view/creating-auth-scheme-in-aspnet-core-2

https://carsonwah.github.io/http-authentication.html

 

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo