ASP.NET Core Development Notes: Adding Login Protection to Swagger UI
Not only should SwaggerUI be accessible after project deployment, but a certain level of security must also be ensured. The approach outlined in this article may offer a relatively simple yet effective solution.
目录9 个章节
Preface
Adding login authentication to SwaggerUI is something I did a long time ago, but my previous approach always felt a bit like hardcoding. Recently, .NET 8 introduced a new feature: calling MapSwagger().RequireAuthorization to protect the Swagger UI. However, this official feature feels half-baked, as it only allows requests with the Authorization header using tools like Postman or curl. Opening it directly in a browser results in a 401 error...
As it happens, a project needed this functionality, so I took the SwaggerUI login authentication middleware I had previously built and refactored it.
This time, I still used Basic Auth for login and wrote a custom SwaggerAuthenticationHandler, implementing the login through the extension methods provided by Microsoft.AspNetCore.Authentication.
Note: This article uses the Single Sign-On authentication project (IdentityServerLite) I recently developed as an example.
Configuring Swagger
This time, I'll try to introduce the topic from the user's perspective rather than following the code's sequential order, which might be more intuitive.
Edit the file src/IdsLite.Api/Extensions/CfgSwagger.cs (as the name suggests, this contains extension methods for configuring Swagger).
public static class CfgSwagger {
public static IServiceCollection AddSwagger(this IServiceCollection services) {
services.AddSwaggerGen();
return services;
}
public static IApplicationBuilder UseSwaggerWithAuthorize(this IApplicationBuilder app) {
app.UseMiddleware<SwaggerBasicAuthMiddleware>();
app.UseSwagger();
app.UseSwaggerUI();
return app;
}
}
The rest is standard configuration. The key point is the line app.UseMiddleware<SwaggerBasicAuthMiddleware>();, which adds a middleware.
SwaggerBasicAuth Middleware
To implement this middleware, the code path is src/IdsLite.Api/Middlewares/SwaggerBasicAuthMiddleware.cs
public class SwaggerBasicAuthMiddleware {
private readonly RequestDelegate _next;
public SwaggerBasicAuthMiddleware(RequestDelegate next) {
_next = next;
}
public async Task InvokeAsync(HttpContext context) {
if (context.Request.Path.StartsWithSegments("/swagger")) {
var result = await context.AuthenticateAsync(AuthSchemes.Swagger);
if (!result.Succeeded) {
context.Response.Headers["WWW-Authenticate"] = "Basic";
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
}
await _next(context);
}
}
The main logic resides in the InvokeAsync method.
It checks if the current address starts with /swagger, then enters the authentication (身份认证) process. If other SwaggerUI addresses are configured, remember to synchronize the configuration of this middleware accordingly, or make it a general configuration to avoid hardcoding.
Here, it uses the extension method context.AuthenticateAsync("Scheme Name") provided by Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions to verify identity (the specific scheme will be implemented later).
If verification fails, it returns a 401 status code and adds the response header WWW-Authenticate:Basic, which will prompt a dialog box in the browser for entering a username and password.
AuthenticationScheme
When registering the Authentication service, you can add some additional schemes.
PS: AspNetCore's Identity system is indeed a bit complex. Even after using it for so long, I still don't feel like I have a systematic understanding of the Identity framework.
Registering Services
The code for registering services looks something like this:
services
.AddAuthentication(options => {
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(...)
.AddScheme<AuthenticationSchemeOptions, SwaggerAuthenticationHandler>(AuthSchemes.Swagger, null);
The AddScheme method can be used to add various types of authentication schemes. Here, a custom authentication scheme SwaggerAuthenticationHandler has been added. The subsequent parameters are the scheme's name and options.
To avoid hard-coding, I wrote a static class:
public static class AuthSchemes {
public const string Swagger = "SwaggerAuthentication";
}
SwaggerAuthenticationHandler
Next, implement this custom authentication scheme. It essentially combines Basic Authentication with fixed usernames and passwords.
However, to avoid hardcoding in the code, I placed the username and password in configuration and obtained them through injection of IOption<T>. They can also be stored in a database and read through something like EFCore.
public class SwaggerAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> {
public SwaggerAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) {}
public SwaggerAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) {}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
if (!Request.Headers.TryGetValue("Authorization", out var value)) {
return AuthenticateResult.Fail("Missing Authorization Header");
}
var config = Context.RequestServices.GetRequiredService<IOptions<IdsLiteConfig>>().Value;
try {
var authHeader = AuthenticationHeaderValue.Parse(value);
var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
var credentials = Encoding.UTF8.GetString(credentialBytes).Split(":", 2);
var username = credentials[0];
var password = credentials[1];
if (username != config.Swagger.UserName || password != config.Swagger.Password) {
return AuthenticateResult.Fail("Invalid Username or Password");
}
var claims = new[] { new Claim(ClaimTypes.Name, username) };
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
catch {
return AuthenticateResult.Fail("Invalid Authorization Header");
}
}
}
The code inside the try block reads the Basic Authentication username and password from the request header (usually Base64 encoded), decodes it, verifies correctness, and then returns the authentication result.
Extensions
You can also integrate OpenIDConnect and OAuth, which I haven't tried yet. For details, please refer to the reference materials.
Summary
To access SwaggerUI after project deployment while maintaining a certain level of security, the approach provided in this article may be a relatively simple and effective solution.
Reference Materials
_EOF_
ASP.NET Core Development Notes: Adding Login Protection to Swagger UI
- 本文作者:
- 程序设计实验室
- 版权声明:
- 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
相关文章
Asp-Net-Core开发笔记:使用原生的接口限流功能
试用了一下,感觉还是太折腾,用回原来的 AspNetCoreRateLimit 组件得了
2024-05-22 · Asp-Net-Core开发笔记 · 约 1 分钟
Asp-Net-Core开发笔记:实现动态审计日志功能
审计日志不仅仅是一种遵循最佳实践的手段,更是确保数据安全、提高系统透明度、促进责任归属明晰的关键工具。
2024-03-21 · Asp-Net-Core开发笔记 · 约 1 分钟
Asp-Net-Core开发笔记:接口返回json对象出现套娃递归问题
前言 看了下推送记录,一个月前,OK,我又变成月更了o(╯□╰)o,这绝对不行![○・`Д´・ ○] 所以今天来更新了 其实不是我懒得更新或者是太忙,其实是最近在写一篇很长的博客,一直没写完( Ĭ ^ Ĭ ) 好吧,先进入正题…… 有一个关于WebApi序列化的问题,跟设计有关,但在涉及到关联字段的时候经常会遇到。 实体类 先看看实体类定义,限于篇幅,只保留几个关键字段。 public class
2022-02-16 · Asp-Net-Core开发笔记 · 约 1 分钟
Asp-Net-Core开发笔记:使用RateLimit中间件实现接口限流
前言 最近一直在忙(2月份沉迷steam,3月开始工作各种忙),好久没更新博客了,不过也积累了一些,忙里偷闲记录一下。 这个需求是这样的,我之前做了个工单系统,现在要对登录、注册、发起工单这些功能做限流,不能让用户请求太频繁。 从 .Net7 开始,已经有内置的限流功能了,但目前我们的项目还在使用 .Net6 LTS 版本,下一个 LTS 没发布之前,暂时不考虑使用 .Net7 这种非 LTS 版
2023-03-20 · Asp-Net-Core开发笔记 · 约 1 分钟
讨论
共 0 条加载评论中…