2020年8月13日 星期四

[ASP.Net Core] Swagger - Security schema for protected APIs


 ASP.NET Core   Swagger   Authorized   Protected API  





Introduction


The OpenAPI Specification from Swagger doesn’t have Authorization information on protected APIs in default.
In this article, we will try to add the Authorization information to the OpenAPI Specification and enable entering JWT for trying the request(s) on Swagger.

The result will be like this,

1.  Swagger show [Authorize] button that will pop up an input modal for Authentication information.
2.  The protected API(s) will shows a lock icon.




A demo:




Related articles




Environment


Dotnet Core SDK 3.1.301
Swashbuckle.AspNetCore 5.4.1



Implement


The sample code can be found on my Github.

Install Nuget packages

Name
Description
Version (In this tutorial)
Swagger tools for documenting APIs built on ASP.NET Core.
5.4.1
A service API versioning library for Microsoft ASP.NET Core.
4.1.1
ASP.NET Core MVC API explorer functionality for discovering metadata such as the list of API-versioned controllers and actions, and their URLs and allowed HTTP methods.
4.1.1
(Optional)
ASP.NET Core middleware that enables an application to receive an OpenID Connect bearer token.
3.1.6


Basic Swagger configuration

If you are on a new ASP.NET Core project, pls follow the steps on this tutorial to complete the basic Swagger configuration.

I will make a quick sample codes as following.

SwaggerConfig.cs

public class SwaggerConfig : IConfigureOptions<SwaggerGenOptions>
{
        private readonly IApiVersionDescriptionProvider provider;

        public SwaggerConfig(IApiVersionDescriptionProvider provider)
        {
            this.provider = provider;
        }

        public void Configure(SwaggerGenOptions options)
        {
            foreach (var description in this.provider.ApiVersionDescriptions)
            {
                var info = new Microsoft.OpenApi.Models.OpenApiInfo()
                {
                    Title = $"MyApp {description.ApiVersion}",
                    Version = description.ApiVersion.ToString(),
                };

                if (description.IsDeprecated)
                {
                    info.Description += " This API version has been deprecated.";
                }

                options.SwaggerDoc(description.GroupNameinfo);
            }
        }
}


Startup.cs: ConfigureServices

The OperationFilter which implements Swashbuckle.AspNetCore.SwaggerGen.IOperationFilter is for binding the parameters from OpenApiParamAttribute to OpenAPI document.

public void ConfigureServices(IServiceCollection services)
{
            #region API Versioning

            services.AddApiVersioning(opt =>
            {
                opt.ReportApiVersions = true// List supported versons on Http header
                opt.DefaultApiVersion = new ApiVersion(10); // Set the default version
                opt.AssumeDefaultVersionWhenUnspecified = true// Use the api of default version
                opt.ApiVersionSelector = new CurrentImplementationApiVersionSelector(opt); // Use the api of latest release number
            });
            #endregion

            #region API Document (Swagger)

            services.AddVersionedApiExplorer(options => options.GroupNameFormat = "'v'VVV");
            services.AddTransient<IConfigureOptions<SwaggerGenOptions>, SwaggerConfig>();
            services.AddSwaggerGen(c =>
            {
                // Set the comments path for the Swagger JSON and UI.
                var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
                var xmlPath = System.IO.Path.Combine(System.AppContext.BaseDirectoryxmlFile);
                c.IncludeXmlComments(xmlPath);
            });
            #endregion
}


Startup.cs: Configure

public void Configure(
            IApplicationBuilder app,
            IApiVersionDescriptionProvider provider)
{
            app.UseSwagger();
            app.UseSwaggerUI(options =>
            {
                foreach (var description in provider.ApiVersionDescriptions)
                {
                    options.SwaggerEndpoint(
                        $"/swagger/{description.GroupName}/swagger.json",
                        description.GroupName.ToUpperInvariant());
                }
            });
}

With the above codes, we can have a basic Swagger UI.
Next we will focus on how to add the Authorization information on protected APIs.




Add Security Scheme to Swagger

When configuring the Swagger generation options, add Security Definition and Security Requirement with our custom JWT Security Scheme.

A Security Definition describes how your API is protected.
A Security Requirement defines a dictionary of required schemes.

services.AddSwaggerGen(c =>
 {
                // … skip
                
                // Add JWT Authentication
                // See https://swagger.io/docs/specification/authentication/bearer-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.IdsecurityScheme);

                // Note: The following configuration will make ALL APIs as protected!
                c.AddSecurityRequirement(new OpenApiSecurityRequirement
                {
                   {securitySchemenew string[] { }}
                });
            });


However, adding the global Security Requirement will makes all the APIs as protected APIs on Swagger, even if some of them are allow-anonymous.




If not all of our APIs are protected (in other words, need to be authorized by JWT), we have not to add the Security Requirement globally.

Lets remove the following codes and we will create custom OperationFilter for only adding Security Requirement on protected APIs.

// Note: The following configuration will make ALL APIs as protected!
// // c.AddSecurityRequirement(new OpenApiSecurityRequirement
// // {
// //    {securityScheme, new string[] { }}
// // });


Create Custom OperationFilter

Here we create a custom OperationFilter that only apply Security Requirement to protected APIs.
Since a protected API has AuthorizeAttribute defined on it as following,

[HttpPost("RevokeToken")]
[Authorize]
public async Task<stringRevokeToken([FromBodystring token)

We can distinguish the protected APIs by it.


AuthorizationOperationFilter.cs

public class AuthorizationOperationFilter : IOperationFilter
{
        public void Apply(OpenApiOperation operationOperationFilterContext 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 response types on secure APIs
                operation.Responses.Add("401"new OpenApiResponse { Description = "Unauthorized" });
                operation.Responses.Add("403"new OpenApiResponse { Description = "Forbidden" });

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

                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
                        }
                    }
                };
            }
        }
}


And we can apply the OperationFilter to Swagger Generation Configuration,

services.AddSwaggerGen(c =>
{
                // … skip

                // Set the custom operation filter
                c.OperationFilter<AuthorizationOperationFilter>();

                // Add JWT Authentication
                // See https://swagger.io/docs/specification/authentication/bearer-authentication/
                var securityScheme = new OpenApiSecurityScheme
                {
                    Name = "JWT Authentication",
                    Description = "Enter JWT Bearer token **_only_**",
                    In = ParameterLocation.Header,
                    Type = SecuritySchemeType.Http,
                    Scheme = AuthenticationScheme.Bearer,
                    BearerFormat = "JWT",
                    Reference = new OpenApiReference
                    {
                        Id = AuthenticationScheme.Bearer,
                        Type = ReferenceType.SecurityScheme
                    }
                };
                c.AddSecurityDefinition(securityScheme.Reference.IdsecurityScheme);

            });
}


Now we only have the lock icon on protected APIs, not all of the APIs.




By clicking the [Authorize] button or the lock icon, we can enter the JWT to access the protected API on Swagger.











We can see that when trying the request on Swagger, the JWT will be added to the Authorization header, and so that we can access the protected API with no problem.






(Optional) For Basic Authentication


Here are the sample codes for adding Security Scheme of Basic Authentication to Swagger.

Startup.cs: ConfigureServices

public void ConfigureServices(IServiceCollection services)
{
   // … skip

   services.AddSwaggerGen(c =>
            {
                // Set the comments path for the Swagger JSON and UI.
                var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
                var xmlPath = System.IO.Path.Combine(System.AppContext.BaseDirectoryxmlFile);
                c.IncludeXmlComments(xmlPath);

                // Set the custom operation filter
                c.OperationFilter<AuthorizationOperationFilter>();

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


AuthorizationOperationFilter.cs

public class AuthorizationOperationFilter : IOperationFilter
{
        public void Apply(OpenApiOperation operationOperationFilterContext 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 response types on secure APIs
                operation.Responses.Add("401"new OpenApiResponse { Description = "Unauthorized" });
                operation.Responses.Add("403"new OpenApiResponse { Description = "Forbidden" });

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

                operation.Security = new List<OpenApiSecurityRequirement>()
                {
                    new OpenApiSecurityRequirement()
                    {
                        {
                            new OpenApiSecurityScheme
                            {
                                Reference = new OpenApiReference
                                {
                                    Id = "basic",
                                    Type = ReferenceType.SecurityScheme
                                }
                            },
                            securityInfos
                        }
                    }
                };
            }
        }
}


The Basic Authentication popup is like this,





Reference










沒有留言:

張貼留言