Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use the [Authorize]-attribute on Swagger UI with ASP.NET Core Swashbuckle

In ASP.NET Core, using Swashbuckle.AspNetCore, how do I protect the access to my Swagger UI in the same way as decorating it with the [Authorize]-attribute?

I want the (equivalent of) [Authorize]-attribute to execute, like for a normally decorated controller/action, when someone tries to access the /swagger-URL on my web-app, so that my custom AuthenticationHandler<T> is executed.

like image 388
Seb Nilsson Avatar asked Oct 08 '19 09:10

Seb Nilsson


People also ask

How do I add authorization in Swagger UI net core?

Create an ASP.NET Core Web API project in Visual Studio 2022 Click on “Create new project.” In the “Create new project” window, select “ASP.NET Core Web API” from the list of templates displayed. Click Next. In the “Configure your new project” window, specify the name and location for the new project.

How do I Authorize Swagger UI?

In the Swagger Editor (the right pane), click the Authorize button, paste the sample API key shown in the description into the Value field (or use your own OpenWeatherMap API key), and click Authorize. Then click Close to close the authorization modal.

How does Authorize attribute work in ASP.NET Core?

Authorization in ASP.NET Core is controlled with AuthorizeAttribute and its various parameters. In its most basic form, applying the [Authorize] attribute to a controller, action, or Razor Page, limits access to that component to authenticated users. Now only authenticated users can access the Logout function.


3 Answers

You could achieve this with a simple middleware solution

Middleware

public class SwaggerAuthenticationMiddleware : IMiddleware
{
    //CHANGE THIS TO SOMETHING STRONGER SO BRUTE FORCE ATTEMPTS CAN BE AVOIDED
    private const string UserName = "TestUser1";
    private const string Password = "TestPassword1";

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        //If we hit the swagger locally (in development) then don't worry about doing auth
        if (context.Request.Path.StartsWithSegments("/swagger") && !IsLocalRequest(context))
        {
            string authHeader = context.Request.Headers["Authorization"];
            if (authHeader != null && authHeader.StartsWith("Basic "))
            {
                // Get the encoded username and password
                var encodedUsernamePassword = authHeader.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)[1]?.Trim();

                // Decode from Base64 to string
                var decodedUsernamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(encodedUsernamePassword));

                // Split username and password
                var username = decodedUsernamePassword.Split(':', 2)[0];
                var password = decodedUsernamePassword.Split(':', 2)[1];

                // Check if login is correct
                if (IsAuthorized(username, password))
                {
                    await next.Invoke(context);
                    return;
                }
            }

            // Return authentication type (causes browser to show login dialog)
            context.Response.Headers["WWW-Authenticate"] = "Basic";

            // Return unauthorized
            context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
        }
        else
        {
            await next.Invoke(context);
        }
    }

    private bool IsAuthorized(string username, string password) => UserName == username && Password == password;

    private bool IsLocalRequest(HttpContext context)
    {
        if(context.Request.Host.Value.StartsWith("localhost:"))
            return true;

        //Handle running using the Microsoft.AspNetCore.TestHost and the site being run entirely locally in memory without an actual TCP/IP connection
        if (context.Connection.RemoteIpAddress == null && context.Connection.LocalIpAddress == null)
            return true;

        if (context.Connection.RemoteIpAddress != null && context.Connection.RemoteIpAddress.Equals(context.Connection.LocalIpAddress))
            return true;

        return IPAddress.IsLoopback(context.Connection.RemoteIpAddress);
    }
}

In startup -> Configure (make sure you add swagger stuff after authentication and authorization)

        app.UseAuthentication();
        app.UseAuthorization();

        //Enable Swagger and SwaggerUI
        app.UseMiddleware<SwaggerAuthenticationMiddleware>(); //can turn this into an extension if you ish
        app.UseSwagger();
        app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "my test api"));

In Startup -> ConfigureServices register the middleware

services.AddTransient<SwaggerAuthenticationMiddleware>();
like image 108
Ricky Gummadi Avatar answered Oct 06 '22 00:10

Ricky Gummadi


The Swagger Middleware is fully independent from the MVC pipeline, so it's not possible out of the box. However, with a bit of reverse engineering, I found a workaround. It involves re-implementing most of the middleware in a custom controller, so it's a bit involved, and obviously it can break with a future update.

First, we need to stop calling IApplicationBuilder.UseSwagger and IApplicationBuilder.UseSwaggerUI, so that it doesn't conflict with our controller.

Then, we must add everything that was added by those methods by modifying our Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("documentName", new Info { Title = "My API", Version = "v1" });
    });

    // RouteTemplate is no longer used (route will be set via the controller)
    services.Configure<SwaggerOptions>(c =>
    {
    });

    // RoutePrefix is no longer used (route will be set via the controller)
    services.Configure<SwaggerUIOptions>(c =>
    {
        // matches our controller route
        c.SwaggerEndpoint("/swagger/documentName/swagger.json", "My API V1");
    });
}


public void Configure(IApplicationBuilder app)
{
    // we need a custom static files provider for the Swagger CSS etc..
    const string EmbeddedFileNamespace = "Swashbuckle.AspNetCore.SwaggerUI.node_modules.swagger_ui_dist";
    app.UseStaticFiles(new StaticFileOptions
    {
        RequestPath = "/swagger", // must match the swagger controller name
        FileProvider = new EmbeddedFileProvider(typeof(SwaggerUIMiddleware).GetTypeInfo().Assembly, EmbeddedFileNamespace),
    });
}

Finally, there are two things to re-implement: the generation of the swagger.json file, and the generation of the swagger UI. We do this with a custom controller:

[Authorize]
[Route("[controller]")]
public class SwaggerController : ControllerBase
{
    [HttpGet("{documentName}/swagger.json")]
    public ActionResult<string> GetSwaggerJson([FromServices] ISwaggerProvider swaggerProvider, 
        [FromServices] IOptions<SwaggerOptions> swaggerOptions, [FromServices] IOptions<MvcJsonOptions> jsonOptions,
        [FromRoute] string documentName)
    {
        // documentName is the name provided via the AddSwaggerGen(c => { c.SwaggerDoc("documentName") })
        var swaggerDoc = swaggerProvider.GetSwagger(documentName);

        // One last opportunity to modify the Swagger Document - this time with request context
        var options = swaggerOptions.Value;
        foreach (var filter in options.PreSerializeFilters)
        {
            filter(swaggerDoc, HttpContext.Request);
        }

        var swaggerSerializer = SwaggerSerializerFactory.Create(jsonOptions);
        var jsonBuilder = new StringBuilder();
        using (var writer = new StringWriter(jsonBuilder))
        {
            swaggerSerializer.Serialize(writer, swaggerDoc);
            return Content(jsonBuilder.ToString(), "application/json");
        }
    }

    [HttpGet]
    [HttpGet("index.html")]
    public ActionResult<string> GetSwagger([FromServices] ISwaggerProvider swaggerProvider, [FromServices] IOptions<SwaggerUIOptions> swaggerUiOptions)
    {
        var options = swaggerUiOptions.Value;
        var serializer = CreateJsonSerializer();

        var indexArguments = new Dictionary<string, string>()
        {
            { "%(DocumentTitle)", options.DocumentTitle },
            { "%(HeadContent)", options.HeadContent },
            { "%(ConfigObject)", SerializeToJson(serializer, options.ConfigObject) },
            { "%(OAuthConfigObject)", SerializeToJson(serializer, options.OAuthConfigObject) }
        };

        using (var stream = options.IndexStream())
        {
            // Inject arguments before writing to response
            var htmlBuilder = new StringBuilder(new StreamReader(stream).ReadToEnd());
            foreach (var entry in indexArguments)
            {
                htmlBuilder.Replace(entry.Key, entry.Value);
            }

            return Content(htmlBuilder.ToString(), "text/html;charset=utf-8");
        }
    }

    private JsonSerializer CreateJsonSerializer()
    {
        return JsonSerializer.Create(new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver(),
            Converters = new[] { new StringEnumConverter(true) },
            NullValueHandling = NullValueHandling.Ignore,
            Formatting = Formatting.None,
            StringEscapeHandling = StringEscapeHandling.EscapeHtml
        });
    }

    private string SerializeToJson(JsonSerializer jsonSerializer, object obj)
    {
        var writer = new StringWriter();
        jsonSerializer.Serialize(writer, obj);
        return writer.ToString();
    }
}
like image 30
Métoule Avatar answered Oct 06 '22 01:10

Métoule


Well, i found an easy solution to the problem. You need to do the following:

  • Implement a middleware. If you have an existing one, you can use that.
  • app.UseSwagger() should be called after app.UseAuthentication().
  • In the middleware Invoke method, just check the path for swagger and redirect the user to the home page/other pages under layout, etc which has authentication enabled or just write a message like "Not authorized" and return.
  • This is far better than an attribute since you can stop the user before reaching the controller and it can easily be scaled to all the controllers.

Hope this helps.

like image 21
Hemendra Singh Avatar answered Oct 05 '22 23:10

Hemendra Singh