Asp.NET Core 2.2: Swagger endpoint specific security definition
Asked Answered
U

2

8

I am using Swashbuckle.AspNetCore 5.0.0-rc2 in one of my .Net Core 2.2 REST projects. Within my project I am serving two different apis which are logically connected to each other.

Today, I managed to seperate my swagger documentations to have one swagger endpoint per api containing only the corresponding api controllers.

I managed to do this by adding a specified group name to the api explorer settings of the controllers:

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ApiExplorerSettings(GroupName = "contracts")]
public class ContractsController : BaseController

[Authorize(AuthenticationSchemes = "BasicAuthentication")]
[ApiExplorerSettings(GroupName = "clearing")]
public class ClearingController : BaseController

With that settings I was able to specify to different endpoints for swagger within my Startup.cs

  // Enable documentation middleware
  app.UseSwagger(so =>
  {
    so.RouteTemplate = "api/doc/{documentName}/swagger.json";
  });
  app.UseSwaggerUI(suo =>
  {
    suo.SwaggerEndpoint("/api/doc/contracts/swagger.json", "Contracts API");
    suo.SwaggerEndpoint("/api/doc/clearing/swagger.json", "Clearing API");
    suo.RoutePrefix = "api/doc";
    suo.SupportedSubmitMethods(SubmitMethod.Get, SubmitMethod.Post, SubmitMethod.Patch, SubmitMethod.Delete);
  });

That worked and everything was fine.

Now as you probably noticed, I am using different authorization methods for the controllers of each api. The first, the contracts api, is using a JWT Token authorization, while the second one, the clearing api is using a Basic authorization.

I thought, the swagger ui would automatically use the correct Authorization method by the "Authorize" Attribute, but I was wrong.

Well I added both authorization methods to the swagger ui middleware like this:

  options.AddSecurityDefinition("Bearer", GetSwaggerTokenSecurityScheme());
  options.AddSecurityDefinition("Basic", GetSwaggerBasicSecurityScheme());

  options.AddSecurityRequirement(GetSwaggerJwtSecurityRequirement());
  options.AddSecurityRequirement(GetSwaggerBasicSecurityRequirement());

Heres my full swagger configuration code:

/// <summary>
/// Configures the swagger generation
/// </summary>
/// <param name="config">The swagger configuration</param>
/// <param name="options">The swagger gen options instance</param>
public static void ConfigureSwaggerGen(IConfiguration config, SwaggerGenOptions options)
{
  var swaggerConfig = config.Get<SwaggerConfiguration>();
  AddSwaggerDocPerApiType(swaggerConfig, options);

  options.AddSecurityDefinition("Bearer", GetSwaggerTokenSecurityScheme());
  options.AddSecurityDefinition("Basic", GetSwaggerBasicSecurityScheme());

  options.AddSecurityRequirement(GetSwaggerJwtSecurityRequirement());
  options.AddSecurityRequirement(GetSwaggerBasicSecurityRequirement());

  if (!swaggerConfig.SwaggerIncludeXml)
  {
    return;
  }
  var xmlFiles = Directory.GetFiles(AppContext.BaseDirectory, "*.xml");
  xmlFiles.ToList().ForEach(f => options.IncludeXmlComments(f));
  options.DescribeAllEnumsAsStrings();
}

/// <summary>
/// Adds a swagger documentation for each api type
/// </summary>
/// <param name="config">The swagger configuration</param>
/// <param name="options">The swagger gen options instance</param>
private static void AddSwaggerDocPerApiType(SwaggerConfiguration config, SwaggerGenOptions options)
{
  options.SwaggerDoc("contracts", GetSwaggerInformationParams(config, "Contracts"));
  options.SwaggerDoc("clearing", GetSwaggerInformationParams(config, "Clearing"));
}

/// <summary>
/// Generates swagger information params object
/// according to the given configuration
/// </summary>
/// <param name="config">The configuration</param>
/// <param name="apiType">The api type</param>
/// <returns>The swagger information</returns>
private static OpenApiInfo GetSwaggerInformationParams(SwaggerConfiguration config, string apiType = "")
{
  var title = string.IsNullOrEmpty(apiType) ? config.SwaggerTitle : apiType;
  var version = string.IsNullOrEmpty(apiType) ? Assembly.GetExecutingAssembly().GetName().Version.ToString() : apiType;

  var swaggerInfo = new OpenApiInfo()
  {
    Title = title,
    Version = version.ToLower(),
    Description = config.SwaggerDescription,
    Contact = new OpenApiContact()
    {
      Name = config.SwaggerCompany,
      Email = config.SwaggerContactMail,
      Url = new Uri(config.SwaggerContactUrl)
    }
  };
  return swaggerInfo;
}

/// <summary>
/// Generates the swagger jwt security scheme object
/// </summary>
/// <returns>The swagger jwt security scheme</returns>
private static OpenApiSecurityScheme GetSwaggerTokenSecurityScheme()
{
  var scheme = new OpenApiSecurityScheme
  {
    Description = "JWT authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
    Name = "JwtAuthorization",
    In = ParameterLocation.Header,
    Type = SecuritySchemeType.ApiKey
  };
  return scheme;
}

/// <summary>
/// Generates the swagger basic security scheme object
/// </summary>
/// <returns>The swagger basic security scheme</returns>
private static OpenApiSecurityScheme GetSwaggerBasicSecurityScheme()
{
  var scheme = new OpenApiSecurityScheme
  {
    Description = "Basic authorization header. Example: \"Authorization: username:password\"",
    Name = "BasicAuthorization",
    In = ParameterLocation.Header,
    Type = SecuritySchemeType.Http,
    Scheme = "basic"
  };
  return scheme;
}

/// <summary>
/// Generates the swagger security scheme object
/// </summary>
/// <returns>The swagger security scheme</returns>
private static OpenApiSecurityRequirement GetSwaggerJwtSecurityRequirement()
{
  var req = new OpenApiSecurityRequirement()
  {
    {
      new OpenApiSecurityScheme()
      {
        Reference = new OpenApiReference() {Type = ReferenceType.SecurityScheme, Id = "Bearer"}
      },
      new[] {"readAccess", "writeAccess"}
    }
  };
  return req;
}

/// <summary>
/// Generates the swagger security scheme object
/// </summary>
/// <returns>The swagger security scheme</returns>
private static OpenApiSecurityRequirement GetSwaggerBasicSecurityRequirement()
{
  var req = new OpenApiSecurityRequirement()
  {
    {
      new OpenApiSecurityScheme()
      {
        Reference = new OpenApiReference() {Type = ReferenceType.SecurityScheme, Id = "Basic"}
      },
      new[] {"readAccess", "writeAccess"}
    }
  };
  return req;
}

Now what I want to achieve is, that only the JWT token authorization is available for the contracts api controllers and only the basic authorization is available for the clearing api controllers.

At the moment I always have both authorization methods available for any api:

enter image description here

Does anybody know how to specify the security for the specific documentation endpoint only?

Best regards

Ustkamenogorsk answered 4/11, 2019 at 13:15 Comment(4)
As per my understanding, you have to skip authorization based on the controller name in the GetSwaggerTokenSecurityScheme or GetSwaggerBasicSecurityScheme.Roderica
The way I used to add the security definitions is global for the swagger documentation. :/ I'm not able to skip them according to api I guess. :/Ustkamenogorsk
Did you find any solution to this? @UstkamenogorskHwu
Unfortunately not, which is the reason why I've separated the documentations.Ustkamenogorsk
R
18

The SwaggerGenOptions.AddSecurityRequirement will apply the Security Requirement globally, so that the security icon (lock icon) and authentication inputs will be applied to all APIs.

Here are the workable solution for me to ONLY apply Security Requirement on protected APIs.

  • Remove SwaggerGenOptions.AddSecurityRequirement from global settings.
  • Create a custom OperationFilter, that implements Swashbuckle.AspNetCore.SwaggerGen.IOperationFilter, and only add SecurityRequirement on protected APIs.
public class AuthorizationOperationFilter : IOperationFilter
{
   public void Apply(OpenApiOperation operation, OperationFilterContext context)
   {
       // Get Authorize attribute
       var attributes = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
                               .Union(context.MethodInfo.GetCustomAttributes(true))
                               .OfType<AuthorizeAttribute>();

       if (attributes != null && attributes.Count() > 0)
       {
          var attr = attributes.ToList()[0];

          // Add what should be show inside the security section
          IList<string> securityInfos = new List<string>();
          securityInfos.Add($"{nameof(AuthorizeAttribute.Policy)}:{attr.Policy}");
          securityInfos.Add($"{nameof(AuthorizeAttribute.Roles)}:{attr.Roles}");
          securityInfos.Add($"{nameof(AuthorizeAttribute.AuthenticationSchemes)}:{attr.AuthenticationSchemes}");

          switch (attr.AuthenticationSchemes)
          {
               case var p when p == AuthenticationScheme.Basic:
                   operation.Security = new List<OpenApiSecurityRequirement>()
                   {
                        new OpenApiSecurityRequirement()
                        {
                            {
                                new OpenApiSecurityScheme
                                {
                                    Reference = new OpenApiReference
                                    {
                                        Id = "basic", // Must fit the defined Id of SecurityDefinition in global configuration
                                        Type = ReferenceType.SecurityScheme,
                                    }
                                },
                                securityInfos
                            }
                        }
                    };
                    break;

                case var p when p == AuthenticationScheme.Bearer: // = JwtBearerDefaults.AuthenticationScheme
                default:
                    operation.Security = new List<OpenApiSecurityRequirement>()
                    {
                        new OpenApiSecurityRequirement()
                        {
                            {
                                new OpenApiSecurityScheme
                                {
                                    Reference = new OpenApiReference
                                    {
                                        Id = "bearer", // Must fit the defined Id of SecurityDefinition in global configuration
                                        Type = ReferenceType.SecurityScheme
                                    }
                                },
                                securityInfos
                            }
                        }
                    };
                    break;
            }
        }
        else
        {
            operation.Security.Clear();
        }
    }
}

Then enable the custom OperationFilter when configuring SwaggerGenOptions:

services.AddSwaggerGen(c =>
{
    
     // Set the custom operation filter
     c.OperationFilter<AuthorizationOperationFilter>();
     
     // Add JWT Authentication
     var securityScheme = new OpenApiSecurityScheme
     {
         Name = "JWT Authentication",
         Description = "Enter JWT Bearer token **_only_**",
         In = ParameterLocation.Header,
         Type = SecuritySchemeType.Http,
         Scheme = "bearer",
         BearerFormat = "JWT",
         Reference = new OpenApiReference
         {
             Id = "bearer",
             Type = ReferenceType.SecurityScheme
         }
     };
     c.AddSecurityDefinition(securityScheme.Reference.Id, securityScheme);

    
     // Add Basic Authentication
     var basicSecurityScheme = new OpenApiSecurityScheme
     {
         Name = "Basic Authentication",
         Type = SecuritySchemeType.Http,
         Scheme = "basic",
         Reference = new OpenApiReference 
         { 
              Id = "basic", 
              Type = ReferenceType.SecurityScheme 
         }
     };
     c.AddSecurityDefinition(basicSecurityScheme.Reference.Id, basicSecurityScheme);
});

Please refer to my article and sample code for more details.

Rendarender answered 13/8, 2020 at 17:23 Comment(0)
L
1

based on the information here:

  1. https://github.com/domaindrivendev/Swashbuckle.AspNetCore#add-security-definitions-and-requirements

  2. https://swagger.io/docs/specification/authentication/

you need to remove the global security requirement added by the statement 'options.AddSecurityRequirement'

and replace it, with security operation defined by the 'options.AddSecurityDefinition' and bind it to the Authorize statement applied to the operations

the link at swagger.io demonstrates various pattens for the security being demanded

hope that helps

+RD

Lief answered 3/3, 2020 at 5:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.