返回文章列表文章索引阅读全文
STARBLOG / 阅读手记
Asp-Net-Core开发笔记

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.

曦远 (DealiAxy)约 18 分钟
目录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 许可协议。转载请注明出处!

相关文章

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…