Swashbuckle add custom header to some methods only - c#

I have successfully add custom header using IOperationFilter in my .net core application, now my problem is how to filter it out for certain methods only in the SomeController class. Is this achievable? This is my current code:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
// some swagger services here.
c.OperationFilter<SomeFilter>();
});
}
SomeFilter.cs
public class SomeFilter: IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (operation.Parameters == null)
operation.Parameters = new List<OpenApiParameter>();
operation.Parameters.Add(new OpenApiParameter
{
Name = "Some-Custom-Header",
In = ParameterLocation.Header,
Required = false,
Schema = new OpenApiSchema
{
Type = "String"
}
});
}
}
SomeController.cs
[ApiController]
[Route("[controller]")]
public class SomeController: Controller
{
[AllowAnonymous]
[HttpPost("some_method1")]
public IAction SomeMethod1() // this method should not include custom header filter
{
return Ok();
}
[Authorize]
[HttpPost("some_method2_with_authorize")]
public IAction SomeMethod2() // this should have the custom header in swagger
{
return Ok();
}
[AllowAnonymous]
[HttpGet("some_method3_without_authorize")]
public IAction SomeMethod3() // this should have the custom header in swagger
{
return Ok();
}
}

I found a way on how to exclude the method that will exclude the said methods by doing this:
public class SomeFilter: IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var methodName = context.ApiDescription.ActionDescriptor.As<ControllerActionDescriptor>().ActionName;
var hasExcludedMethod = ApiFilterSettings.AppSettingsFilterMethods.ToStringList().Contains(methodName);
if(hasExcludedMethod)
return; // return directly when an excluded method is found;**strong text**
if (operation.Parameters == null)
operation.Parameters = new List<OpenApiParameter>();
operation.Parameters.Add(new OpenApiParameter
{
Name = "Some-Custom-Header",
In = ParameterLocation.Header,
Required = false,
Schema = new OpenApiSchema
{
Type = "String"
}
});
}
}
I hope this will help you guys.

Related

IAuthorizationFilter and IOperationFilter with swagger

Summary
What I want is to be able to add a custom AuthorizeAttribute to a method which is then recognised by swagger and displays the Available authorizations popup. The issue I'm having is getting the IOperationFilter to work correctly with the IAuthorizationFilter.
Code
startup.cs
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v2", new OpenApiInfo { Title = "API", Version = "v2" });
// Adds "(Auth)" to the summary so that you can see which endpoints have Authorization
c.OperationFilter<AppendAuthorizeToSummaryOperationFilter<Filters.Authorization2>>();
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Name = "Bearer",
Description = "Standard Authorization header using the SessionKey scheme. Example: \"{token}\"",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey
});
c.OperationFilter<Filters.AuthorizeCheckOperationFilter>();
});
controller.cs
[Filters.Authorization2]
[HttpGet]
public ApiResult<List<Request>> GetRequest(int ID)
Authorization2.cs
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class Authorization2 : AuthorizeAttribute, IAuthorizationFilter
{
public Authorization2()
{
}
public void OnAuthorization(AuthorizationFilterContext context)
{
//Get the session from the cache
Session sess = GetSession(context.HttpContext);
if (sess.IsValid)
{
//set sess
}
else
{
//If it's not there then return with bad news
context.Result = Unauthorized();
context.HttpContext.Response.StatusCode = 401;
}
}
}
AuthorizeCheckOperationFilter.cs
internal class AuthorizeCheckOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
context.ApiDescription.TryGetMethodInfo(out var methodInfo);
if (methodInfo == null)
return;
var hasAuthorizeAttribute = false;
if (methodInfo.MemberType == MemberTypes.Method)
{
// NOTE: Check the controller itself has Authorize attribute
hasAuthorizeAttribute = methodInfo.DeclaringType.GetCustomAttributes(true).OfType<Authorization2>().Any();
// NOTE: Controller has Authorize attribute, so check the endpoint itself.
// Take into account the allow anonymous attribute
if (hasAuthorizeAttribute)
hasAuthorizeAttribute = !methodInfo.GetCustomAttributes(true).OfType<AllowAnonymousAttribute>().Any();
else
hasAuthorizeAttribute = methodInfo.GetCustomAttributes(true).OfType<Authorization2>().Any();
}
if (!hasAuthorizeAttribute)
return;
if (!operation.Responses.Any(r => r.Key == StatusCodes.Status401Unauthorized.ToString()))
operation.Responses.Add(StatusCodes.Status401Unauthorized.ToString(), new OpenApiResponse { Description = "Unauthorized" });
if (!operation.Responses.Any(r => r.Key == StatusCodes.Status403Forbidden.ToString()))
operation.Responses.Add(StatusCodes.Status403Forbidden.ToString(), new OpenApiResponse { Description = "Forbidden" });
// NOTE: This adds the "Padlock" icon to the endpoint in swagger,
// we can also pass through the names of the policies in the string[]
// which will indicate which permission you require.
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header
},
new List<string>()
}
}
};
}
}
With the code as it is, the padlock on the end point shows up and the header is set but the method throws an 500 error:
System.InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found.
Attempt 1
I tried adding:
services.AddAuthentication(Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme);
but I get the same error. If I try:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options =>
{
options.LoginPath = new PathString("/auth/login");
options.AccessDeniedPath = new PathString("/auth/denied");
});
My request redirects to the loginPath, which causes a 404.
Attempt 2
If try a different tacktick and use a TypeFilterAttribute as so:
modify startup.cs
c.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
//c.OperationFilter<Filters.AuthorizeCheckOperationFilter>();
Authorization.cs
public class AuthorizationHandler : IAuthorizationFilter
{
public AuthorizationHandler()
{
}
public void OnAuthorization(AuthorizationFilterContext context)
{
//Get the session from the cache
Session sess = GetSession(context.HttpContext);
if (sess.IsValid)
{
//set sess
}
else
{
//If it's not there then return with bad news
context.Result = Unauthorized();
context.HttpContext.Response.StatusCode = 401;
}
}
}
public class AuthorizeAttribute : TypeFilterAttribute
{
public AuthorizeAttribute() : base(typeof(AuthorizationHandler))
{
}
}
and update the method to use [Filters.Authorize] the call to the method works as expected but now every method get's the lock, not just the ones with the attribute.
Question
How do I modify my code to only have locks on the methods with the attribute and process the authorization correctly?
After following this response to a different question:
https://stackoverflow.com/a/61365691/2885471
I managed to get it to work. In essence I created a security definition in the SwaggerGen service configuration, put the security requirement in a IOperationFilter class and then add the the class to the OperationFilters.

IdentityServer: Multiple Clients with different grant types, policies not working properly

I've got an identity server setup with the following 'look a like' configuration:
return new List<Client>
{
new Client
{
[...]
AllowedGrantTypes = GrantTypes.Implicit,
[....]
},
new Client
{
[...]
AllowedGrantTypes = GrantTypes.ClientCredentials,
[....]
}
};
and controlles annotated like this:
[Route("api/forms")]
[ApiController]
[Authorize(Policy = "user.api.portfolio.manager")]
[Authorize(Policy = "application.api.portfolio.manager")]
public class FormsController : ControllerBase
{
[...]
}
and a policy
private System.Action<AuthorizationOptions> AddJwtAuthorizationPolicyForRole()
{
return options => { options.AddPolicy("**POLICY_FOR_GRANT_IMPLICIT**", policy => {
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.RequireClaim(ClaimTypes.Role, "USER_ACCESSIBLE");
});
};
}
private System.Action<AuthorizationOptions> AddJwtAuthorizationPolicyForRole()
{
return options => { options.AddPolicy("**POLICY_FOR_CLIENT_CREDENTIALS**", policy => {
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
});
};
}
so I want to achieve:
Clients using the GrantType.ClientCredentials can access the controller without any further needs.
Clients using the Implicit Schema must have role USER_ACCESSIBLE
If it's configured like shown above, both policies must apply -> Both grant types are failing.
How can I achieve the described behavior using IdentityServer, that each grant-types may have an independent policy so be applied?
Thanks in advance for your help.
The most simplest solution is adding another single policy for Implicit + ClientCredential to implement logics for OR conditions .
Or you can create a custom attribute like :
MultiplePolicysAuthorizeAttribute
public class MultiplePolicysAuthorizeAttribute : TypeFilterAttribute
{
public MultiplePolicysAuthorizeAttribute(string policys, bool isAnd = false) : base(typeof(MultiplePolicysAuthorizeFilter))
{
Arguments = new object[] { policys, isAnd };
}
}
MultiplePolicysAuthorizeFilter
public class MultiplePolicysAuthorizeFilter : IAsyncAuthorizationFilter
{
private readonly IAuthorizationService _authorization;
public string Policys { get; private set; }
public bool IsAnd { get; private set; }
public MultiplePolicysAuthorizeFilter(string policys, bool isAnd, IAuthorizationService authorization)
{
Policys = policys;
IsAnd = isAnd;
_authorization = authorization;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var policys = Policys.Split(";").ToList();
if (IsAnd)
{
foreach (var policy in policys)
{
var authorized = await _authorization.AuthorizeAsync(context.HttpContext.User, policy);
if (!authorized.Succeeded)
{
context.Result = new ForbidResult();
return;
}
}
}
else
{
foreach (var policy in policys)
{
var authorized = await _authorization.AuthorizeAsync(context.HttpContext.User, policy);
if (authorized.Succeeded)
{
return;
}
}
context.Result = new ForbidResult();
return;
}
}
}
If actions require matching one of the policies(OR):
[MultiplePolicysAuthorize("POLICY_FOR_GRANT_IMPLICIT;POLICY_FOR_CLIENT_CREDENTIALS")]
If actions require matching all the policies(And) :
[MultiplePolicysAuthorize("POLICY_FOR_GRANT_IMPLICIT;POLICY_FOR_CLIENT_CREDENTIALS",true)]
Code sample reference : https://stackoverflow.com/a/52639938/5751404

Getting error with aspnet core 2.1 action filter due to missing suitable constructor

I have made a claims filter
public class ClaimRequirementAttribute : TypeFilterAttribute
{
public ClaimRequirementAttribute(string claimType, ClaimRoles claimValue) : base(typeof(ClaimRequirementFilter))
{
Arguments = new object[] {new Claim(claimType, claimValue.ToString()) };
}
}
public class ClaimRequirementFilter : IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
var headers = context.HttpContext.Request.Headers;
var tokenSuccess = headers.TryGetValue("Token", out var token);
var emailSuccess = headers.TryGetValue("Email", out var email);
var deviceNameSuccess = headers.TryGetValue("DeviceName", out var deviceName);
if (tokenSuccess && emailSuccess && deviceNameSuccess)
{
var accountLogic = context.HttpContext.RequestServices.GetService<IAccountLogic>();
var hasClaim = accountLogic.ValidateLogin(email, token, deviceName).Result.Success;
if (!hasClaim)
{
context.HttpContext.ForbidAsync();
}
}
else
{
context.HttpContext.ForbidAsync();
}
}
}
I have registered the filter in my startup
public void ConfigureServices(IServiceCollection services)
{
services.Configure<ConnectionStringsSettings>(Configuration.GetSection("ConnectionStrings"));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddScoped<ClaimRequirementFilter>();
But I get this error when I navigate to an action that uses the filter
[HttpPost]
[ClaimRequirement("Permission", ClaimRoles.Admin)]
public async Task ResetLeaderboard()
InvalidOperationException: A suitable constructor for type 'Foosball.Logic.ClaimRequirementFilter' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor
github: https://github.com/Mech0z/Foosball/tree/core2.1/Foosball
As your code has
Arguments = new object[] {new Claim(claimType, claimValue.ToString()) };
you need to add the following constructor:
public ClaimRequirementFilter(Claim claim)
{
}
That is because the internal constructor resolving logic uses TypeFilterAttribute.Argument property to decide what constructor to use for instantiation.

API File Upload using HTTP content not exposed in swagger

I am implementing a swagger interface into an existing web API. The current API controller exposes an async upload function which uses the Request.Content to transport an image asynchronously. The code that has been used is explained in this article.
My api controller:
[HttpPost]
[Route("foo/bar/upload")]
public async Task<HttpResponseMessage> Upload()
{
if (!Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
var provider = await Request.Content.ReadAsMultipartAsync(new InMemoryMultipartFormDataStreamProvider());
NameValueCollection formData = provider.FormData;
HttpResponseMessage response;
//access files
IList<HttpContent> files = provider.Files;
if (files.Count > 0)
{
HttpContent file1 = files[0];
using (Stream input = await file1.ReadAsStreamAsync())
{
object responseObj = ExternalProcessInputStream(input)
response = Request.CreateResponse(HttpStatusCode.OK, responseObj);
}
}
else
{
response = Request.CreateResponse(HttpStatusCode.BadRequest);
}
return response;
}
This works dandy, but when i expose this through swagger i have a parameterless function, which returns an error when used.
My question is how can supply a proper value to test this method with?
You'll need to add a custom IOperationFilter to handle this.
Given you have a controller like so:
[ValidateMimeMultipartContentFilter]
[HttpPost, Route("softwarepackage")]
public Task<SoftwarePackageModel> UploadSingleFile()
{
var streamProvider = new MultipartFormDataStreamProvider(ServerUploadFolder);
var task = Request.Content.ReadAsMultipartAsync(streamProvider).ContinueWith<SoftwarePackageModel>(t =>
{
var firstFile = streamProvider.FileData.FirstOrDefault();
if (firstFile != null)
{
// Do something with firstFile.LocalFileName
}
return new SoftwarePackageModel
{
};
});
return task;
}
You then need to create an Swashbuckle.Swagger.IOperationFilter to add a file upload parameter to your function like:
public class FileOperationFilter : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
if (operation.operationId.ToLower() == "softwarepackage_uploadsinglefile")
{
if (operation.parameters == null)
operation.parameters = new List<Parameter>(1);
else
operation.parameters.Clear();
operation.parameters.Add(new Parameter
{
name = "File",
#in = "formData",
description = "Upload software package",
required = true,
type = "file"
});
operation.consumes.Add("application/form-data");
}
}
}
And in your Swagger config you'll need to register the filter:
config.EnableSwagger(c => {... c.OperationFilter<FileOperationFilter>(); ... });
To top this up, I also added a FilterAttribute to filter out Multipart content:
public class ValidateMimeMultipartContentFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (!actionContext.Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
}
}

AuthorizationHandler exception not going through ExceptionFilter

I have an application in ASP.NET Core MVC (dnx46) RC1 with an AuthorizationHandler:
public class AppSumAuthAuthorizationHandler : AuthorizationHandler<AppSumAuthRequirement>
{
private readonly IUserRepository _userRepository;
private readonly IUserRoleRepository _userRoleRepository;
public AppSumAuthAuthorizationHandler(IUserRepository userRepository, IUserRoleRepository userRoleRepository)
{
_userRepository = userRepository;
_userRoleRepository = userRoleRepository;
}
protected override async void Handle(AuthorizationContext context, AppSumAuthRequirement requirement)
{
await HandleAsync(context,requirement);
}
protected override async Task HandleAsync(AuthorizationContext context, AppSumAuthRequirement requirement)
{
var currentUserName = context.User.Identity.Name;
var currentUser = await _userRepository.GetAsync(u => u.UserName == context.User.Identity.Name);
// Create user that does not yet exist
if(currentUser == null)
{
var user = new User(currentUserName);
/* Temporary add SysAdmin role */
using(new CreatedBySystemProvider(_userRepository))
{
_userRepository.Add(user);
await _userRepository.SaveChangesAsync();
if (string.Equals(currentUserName, #"BIJTJES\NilsG", StringComparison.CurrentCultureIgnoreCase))
{
user.AddRole(1);
}
currentUser = await _userRepository.GetAsync(u => u.Id == user.Id);
}
}
var resource = (Microsoft.AspNet.Mvc.Filters.AuthorizationContext) context.Resource;
var controllerActionDescriptor = resource.ActionDescriptor as ControllerActionDescriptor;
var controllerName = controllerActionDescriptor.ControllerName;
var actionName = controllerActionDescriptor.Name;
string moduleName;
try
{
// Get the name of the module
moduleName = ((ModuleAttribute)controllerActionDescriptor.ControllerTypeInfo.GetCustomAttributes(false).First(a => a.GetType().Name == "ModuleAttribute")).ModuleName;
}
catch(InvalidOperationException ex)
{
context.Fail();
throw new InvalidOperationException($"The Module Attribute is required on basecontroller {controllerName}.", ex);
}
var access = new Access(moduleName, controllerName, actionName);
if (await currentUser.HasPermissionTo(UrlAccessLevel.Access).OnAsync(access))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
}
The requirement class is empty:
public interface IAppSumAuthRequirement : IAuthorizationRequirement
{
}
public class AppSumAuthRequirement : IAppSumAuthRequirement
{
}
The Module attribute is also nothing special:
public class ModuleAttribute : Attribute
{
public string ModuleName { get; private set; }
public ModuleAttribute(string moduleName)
{
ModuleName = moduleName;
}
public override string ToString()
{
return ModuleName;
}
}
The exception filter:
public class JsonExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
var exception = context.Exception;
context.HttpContext.Response.StatusCode = 500;
context.Result = new JsonResult(new Error
{
Message = exception.Message,
InnerException = exception.InnerException?.InnerException?.Message,
Data = exception.Data,
ErrorCode = exception.HResult,
Source = exception.Source,
Stacktrace = exception.StackTrace,
ErrorType = exception.GetType().ToString()
});
}
}
and policy are configured in my Startup.cs:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.Filters.Add(new JsonExceptionFilterAttribute());
options.ModelBinders.Insert(0, new NullableIntModelBinder());
}).AddJsonOptions(options => {
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
options.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();
});
// Security
services.AddAuthorization(options =>
{
options.AddPolicy("AppSumAuth",
policy => policy.Requirements.Add(new AppSumAuthRequirement()));
});
}
and the policy is set on all controllers, by inheriting BaseController:
[Authorize(Policy = "AppSumAuth")]
public class BaseController : Controller
{
public BaseController()
{
}
}
So, in my handler, I get the controllername, actionname and modulename (from the attribute set on the controllers):
[Module("Main")]
When this attribute is not set on a controller, I would like to catch the exception and report this back to the developer calling the controller and deny access. To do this, I've added:
catch(InvalidOperationException ex)
{
context.Fail();
throw new InvalidOperationException($"The Module Attribute is required on basecontroller {controllerName}.", ex);
}
The JsonExceptionFilter is called perfectly when there is an exception in the controllers. It is however not called when there is an error in the AuthorizationHandler.
So the question:
How can I get the Exceptions to be caught by the JsonExceptionFilter?
What am I doing wrong?
Solution:
Startup.cs:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// For Windows Auth!
app.UseIISPlatformHandler();
app.UseStaticFiles();
app.UseExceptionHandler(AppSumExceptionMiddleware.JsonHandler());
app.UseMvc();
}
And my middleware:
public class AppSumExceptionMiddleware
{
public static Action<IApplicationBuilder> JsonHandler()
{
return errorApp =>
{
errorApp.Run(async context =>
{
var exception = context.Features.Get<IExceptionHandlerFeature>();
if (exception != null)
{
var exceptionJson = Encoding.UTF8.GetBytes(
JsonConvert.SerializeObject(new AppSumException(exception.Error),
new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
})
);
context.Response.ContentType = "application/json";
await context.Response.Body.WriteAsync(exceptionJson, 0, exceptionJson.Length);
}
});
};
}
}
Action filter can be used as a method filter, controller filter, or global filter only for MVC HTTP requests. In your case you need to use a middleware, as
Middleware is component that "sit" on the HTTP pipeline and examine
all requests and responses.
As you want to works with exception, you may use ready-to-use ExceptionHandler middleware:
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = 500; // for example
var error = context.Features.Get<IExceptionHandlerFeature>();
if (error != null)
{
var ex = error.Error;
// custom logic
}
});
});

Categories

Resources