Custom AuthorizeAttribute throwing error after OnAuthorization - c#

I want to do a custom AuthorizeAttribute. I want to use the IMemoryCache to store the tokens and i'm using a custom provider to inject the IMemoryCache instance. My problem is after OnAuthorization method it is not called my controller's action and it throws an internal server error that i'm not able to catch.
And here is the implementation so far
public class ApiAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
public IMemoryCache Cache { get; set; }
/// <summary>
/// Verifica se o token é válido na sessão
/// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
public void OnAuthorization(AuthorizationFilterContext context)
{
//Check we have a valid HttpContext
if (context.HttpContext == null)
throw new ArgumentNullException("httpContext");
string token;
token = context.HttpContext.Request.QueryString.Value;
if (String.IsNullOrEmpty(token))
token = context.HttpContext.Request.Form["token"];
if (String.IsNullOrEmpty(token))
{
context.Result = new UnauthorizedResult();
return;
}
if (Cache == null)
{
context.Result = new UnauthorizedResult();
return;
}
if (token.Contains("="))
{
token = token.Split('=')[1];
}
var tokens = Cache.Get<Dictionary<string, User>>("tokens");
var result = (from t in tokens where t.Key == token select t.Value).ToList();
var controller = (string)context.RouteData.Values["controller"];
var action = (string)context.RouteData.Values["action"];
if (result.Count < 1)
context.Result = new UnauthorizedResult();
}
}
public class CacheProvider : IApplicationModelProvider
{
private IMemoryCache _cache;
public CacheProvider(IMemoryCache cache)
{
_cache = cache;
}
public int Order { get { return -1000 + 10; } }
public void OnProvidersExecuted(ApplicationModelProviderContext context)
{
foreach (var controllerModel in context.Result.Controllers)
{
// pass the depencency to controller attibutes
controllerModel.Attributes
.OfType<ApiAuthorizeAttribute>().ToList()
.ForEach(a => a.Cache = _cache);
// pass the dependency to action attributes
controllerModel.Actions.SelectMany(a => a.Attributes)
.OfType<ApiAuthorizeAttribute>().ToList()
.ForEach(a => a.Cache = _cache);
}
}
public void OnProvidersExecuting(ApplicationModelProviderContext context)
{
// intentionally empty
}
}
And here is the controller
[ApiAuthorize]
[HttpPost]
public JsonResult Delete([FromForm] string inputId)
{
//Do stuff
}
Thank in advance

After some digging i found this way to do this, i dont know if it is the best way to achieve that
I Created a policy requirement
public class TokenRequirement : IAuthorizationRequirement
{
}
And an AuthorizationHandler
public class TokenRequirementHandler : AuthorizationHandler<TokenRequirement>
{
public IMemoryCache Cache { get; set; }
public TokenRequirementHandler(IMemoryCache memoryCache)
{
Cache = memoryCache;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TokenRequirement requirement)
{
return Task.Run(() => { //access the cache and then
context.Succeed(requirement); });
}
}
On my Startup i registered the handler and add the Authorization
services.AddAuthorization(options =>
{
options.AddPolicy("Token",
policy => policy.Requirements.Add(new Authorize.TokenRequirement()));
});
services.AddSingleton<IAuthorizationHandler, Authorize.TokenRequirementHandler>();
In the controller i used the Authorize attribute
[Authorize(Policy = "Token")]
[HttpPost]
public JsonResult Delete([FromForm] string inputId)
{
//Do stuff
}
And now it works.
Thank you.

Related

Catching errors from services in Exception Filter

I've created a service that's used throughout my aspnet project that retrieves and validates a header among other things. Issue is that the Exception Filter is not able to catch the errors that are thrown by the service as it's not in the scope of the Exception Filter thus giving the user an ugly internal server error. Is there any way to gracefully return an argument error with description to the user with the use of the services?
The Startup:
services.AddScoped<UserService>();
services
.AddMvc(x =>
{
x.Filters.Add(typeof(Filters.MyExceptionFilter));
})
The Exception Filter:
public class MyExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
if (context.Exception is ArgumentException argumentException)
{
var response = context.HttpContext.Response;
context.ExceptionHandled = true;
response.StatusCode = 400;
context.Result = new ObjectResult(argumentException.Message);
return;
}
}
}
The Service:
public class UserService
{
public readonly string UserId;
public UserService(IHttpContextAccessor context)
{
if (!context.HttpContext.Request.Headers.TryGetValue("x-test", out var user))
{
throw new ArgumentException($"x-test header is required.");
}
UserId = user;
//do other stuff
}
}
Controller Action Method:
public async Task<IActionResult> Delete(string id,
[FromServices] UserService userService)
{
//do stuff
}
A rule-of-thumb in C# is do as little work in the constructor as possible. There's a few good reasons for this (e.g. you can't dispose a class that threw an exception in it's constructor). Another good reason is, as you have found out, construction might happen in a different place (e.g. a DI container) than the place you actually use the class.
The fix should be quite straightforward - just move the logic out of the constructor. You could use a Lazy<T> to do this for example:
public class UserService
{
public readonly Lazy<string> _userId ;
public UserService(IHttpContextAccessor context)
{
_userId = new Lazy<string>(() =>
{
if (!context.HttpContext.Request.Headers.TryGetValue("x-test", out var user))
{
throw new ArgumentException($"x-test header is required.");
}
return user;
});
//do other stuff
}
public string UserId => _userId.Value;
}
Or you could just get the value when you needed it:
public class UserService
{
public readonly IHttpContextAccessor _context;
public UserService(IHttpContextAccessor context)
{
_context = context;
//do other stuff
}
public string UserId
{
get
{
if (_context.HttpContext.Request.Headers.TryGetValue("x-test", out var user))
{
return user;
}
else
{
throw new ArgumentException($"x-test header is required.");
}
}
}
}

how do I cache an output in ASP.NET Core

I have a API controller,and the scenario is:
I need to consume third party datasource(let's say the third party is provided as a dll file for simplicity, and the dll contain Student model and StudentDataSource that contain a lot of method to retrieve student ), and calling the third party data source is costly and data only gets updated every 6 hours.
so somehow I need to cache the output, below is some action method from my api controller:
// api controller that contain action methods below
[HttpGet]
public JsonResult GetAllStudentRecords()
{
var dataSource = new StudentDataSource();
return Json(dataSource.GetAllStudents());
}
[HttpGet("{id}")]
public JsonResult GetStudent(int id)
{
var dataSource = new StudentDataSource();
return Json(dataSource.getStudent(id));
}
then how should I cache the result especially for the second action method, it is dumb to cache every student result with different id
My team is implementing a similar caching strategy on an API controller using a custom Action filter attribute to handle the caching logic. See here for more info on Action filters.
The Action filter's OnActionExecuting method runs prior to your controller method, so you can check whether the data you're looking for is already cached and return it directly from here, bypassing the call to your third party datasource when cached data exists. We also use this method to check the type of request and reset the cache on updates and deletes, but it sounds like you won't be modifying data.
The Action filter's OnActionExecuted method runs immediately AFTER your controller method logic, giving you an opportunity to cache the response object before returning it to the client.
The specifics of how you implement the actual caching are harder to provide an answer for, but Microsoft provides some options for in-memory caching in .NET Core (see MemoryCache.Default not available in .NET Core?)
I used the solution with the cache strategy through the controller API as #chris-brenberg pointed out, it turned out like this
on controller class
[ServerResponseCache(false)]
[HttpGet]
[Route("cache")]
public ActionResult GetCache(string? dateFormat) {
Logger.LogInformation("Getting current datetime");
return Ok(new { date = DateTime.Now.ToString() });
}
on ServerResponseCacheAttribute.cs
namespace Site.Api.Filters {
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Primitives;
using System.Globalization;
using System.Threading.Tasks;
public class ServerResponseCacheAttribute : TypeFilterAttribute {
public ServerResponseCacheAttribute(bool byUserContext = true) : base(typeof(ServerResponseCacheAttributeImplementation)) =>
Arguments = new object[] { new ServerResponseCacheProps { ByUserContext = byUserContext } };
public ServerResponseCacheAttribute(int secondsTimeout, bool byUserContext = true) : base(typeof(ServerResponseCacheAttributeImplementation)) =>
Arguments = new object[] { new ServerResponseCacheProps { SecondsTimeout = secondsTimeout, ByUserContext = byUserContext } };
public class ServerResponseCacheProps {
public int? SecondsTimeout { get; set; }
public bool ByUserContext { get; set; }
}
public class ServerResponseCacheConfig {
public bool Disabled { get; set; }
public int SecondsTimeout { get; set; } = 60;
public string[] HeadersOnCache { get; set; } = { "Accept-Language" };
}
private class ServerResponseCacheAttributeImplementation : IAsyncActionFilter {
private string _cacheKey = default;
readonly ILogger<ServerResponseCacheAttributeImplementation> _logger;
readonly IMemoryCache _memoryCache;
readonly ServerResponseCacheConfig _config;
readonly bool _byUserContext;
public ServerResponseCacheAttributeImplementation(ILogger<ServerResponseCacheAttributeImplementation> logger,
IMemoryCache memoryCache, ServerResponseCacheProps props) {
_logger = logger;
_memoryCache = memoryCache;
_byUserContext = props.ByUserContext;
_config = new ServerResponseCacheConfig {
SecondsTimeout = props.SecondsTimeout ?? 60,
HeadersOnCache = new[] { "Accept-Language" }
};
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) {
if (context == null) {
throw new ArgumentNullException(nameof(context));
}
if (next == null) {
throw new ArgumentNullException(nameof(next));
}
if (_config.Disabled) {
await next();
return;
}
OnActionExecutingAsync(context);
if (context.Result == null) {
OnActionExecuted(await next());
}
}
void OnActionExecutingAsync(ActionExecutingContext context) {
SetCacheKey(context.HttpContext.Request);
// Not use a stored response to satisfy the request. Will regenerates the response for the client, and updates the stored response in its cache.
bool noCache = context.HttpContext.Request.Headers.CacheControl.Contains("no-cache");
if (noCache) {
return;
}
TryLoadResultFromCache(context);
}
void SetCacheKey(HttpRequest request) {
if (request == null) {
throw new ArgumentException(nameof(request));
}
if (!string.Equals(request.Method, "GET", StringComparison.InvariantCultureIgnoreCase)) {
return;
}
List<string> cacheKeys = new List<string>();
if (_byUserContext && request.HttpContext.User.Identity.IsAuthenticated) {
cacheKeys.Add($"{request.HttpContext.User.Identity.Name}");
}
string uri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path, request.QueryString);
cacheKeys.Add(uri);
foreach (string headerKey in _config.HeadersOnCache) {
StringValues headerValue;
if (request.Headers.TryGetValue(headerKey, out headerValue)) {
cacheKeys.Add($"{headerKey}:{headerValue}");
}
}
_cacheKey = string.Join('_', cacheKeys).ToLower();
}
void TryLoadResultFromCache(ActionExecutingContext context) {
ResultCache resultCache;
if (_cacheKey != null && _memoryCache.TryGetValue(_cacheKey, out resultCache)) {
_logger.LogInformation("ServerResponseCache: Response loaded from cache, cacheKey: {cacheKey}, expires at: {expiration}.", _cacheKey, resultCache.Expiration);
context.Result = resultCache.Result;
SetExpiresHeader(context.HttpContext.Response, resultCache.Expiration);
}
}
/// <summary>Add expires header (the time after which the response is considered stale).</summary>
void SetExpiresHeader(HttpResponse response, DateTimeOffset expiration) {
string expireHttpDate = expiration.UtcDateTime.ToString("ddd, dd MMM yyyy HH:mm:ss", CultureInfo.InvariantCulture);
response.Headers.Add("Expires", $"{expireHttpDate} GMT");
}
void OnActionExecuted(ActionExecutedContext context) {
if (_cacheKey == null) {
return;
}
if (context.Result != null) {
DateTimeOffset expiration = SetCache(context.Result);
SetExpiresHeader(context.HttpContext.Response, expiration);
} else {
RemoveCache();
}
}
DateTimeOffset SetCache(IActionResult result) {
DateTimeOffset absoluteExpiration = DateTimeOffset.Now.AddSeconds(_config.SecondsTimeout);
ResultCache resultCache = new ResultCache {
Result = result,
Expiration = absoluteExpiration
};
_memoryCache.Set(_cacheKey, resultCache, absoluteExpiration);
_logger.LogInformation("ServerResponseCache: Response set on cache, cacheKey: {cacheKey}, until: {expiration}.", _cacheKey, absoluteExpiration);
return absoluteExpiration;
}
void RemoveCache() {
_memoryCache.Remove(_cacheKey);
_logger.LogInformation("ServerResponseCache: Response removed from cache, cacheKey: {cacheKey}.", _cacheKey);
}
}
private class ResultCache {
public IActionResult Result { get; set; }
public DateTimeOffset Expiration { get; set; }
}
}}
I hope it helps someone, best regards

Custom Authorize attribute - ASP .NET Core 2.2

I want to create a custom Authorize attribute to be able to send a personalized response when it fails. There are many examples, but I could not find what I'm looking for.
When registering a policy, I add a "claim". Is it possible to access that registered claim within the custom attribute without having to pass the claim by parameter? or is it possible to know if the check of the claim happened and if not, return a personalized response? Thx!
public static void AddCustomAuthorization(this IServiceCollection serviceCollection)
{
serviceCollection.AddAuthorization(x =>
{
x.AddPolicy(UserPolicy.Read,
currentPolicy => currentPolicy.RequireClaim(UserClaims.Read));
});
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class CustomAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext authorizationFilterContext)
{
if (authorizationFilterContext.HttpContext.User.Identity.IsAuthenticated)
{
if (!authorizationFilterContext.HttpContext.User.HasClaim(x => x.Value == "CLAIM_NAME")) // ACCESS TO REGISTER CLAIM => currentPolicy => currentPolicy.RequireClaim(UserClaims.Read)
{
authorizationFilterContext.Result = new ObjectResult(new ApiResponse(HttpStatusCode.Unauthorized));
}
}
}
}
[HttpGet]
[CustomAuthorizeAttribute(Policy = UserPolicy.Read)]
public async Task<IEnumerable<UserDTO>> Get()
{
return ...
}
You can use IAuthorizationPolicyProvider to get the policy and then use ClaimsAuthorizationRequirement.ClaimType to get a claim name. And since it has async API, it is better to use IAsyncAuthorizationFilter instead of IAuthorizationFilter. Try this:
public class CustomAuthorizeAttribute : AuthorizeAttribute, IAsyncAuthorizationFilter
{
public async Task OnAuthorizationAsync(AuthorizationFilterContext authorizationFilterContext)
{
var policyProvider = authorizationFilterContext.HttpContext
.RequestServices.GetService<IAuthorizationPolicyProvider>();
var policy = await policyProvider.GetPolicyAsync(UserPolicy.Read);
var requirement = (ClaimsAuthorizationRequirement)policy.Requirements
.First(r => r.GetType() == typeof(ClaimsAuthorizationRequirement));
if (authorizationFilterContext.HttpContext.User.Identity.IsAuthenticated)
{
if (!authorizationFilterContext.HttpContext
.User.HasClaim(x => x.Value == requirement.ClaimType))
{
authorizationFilterContext.Result =
new ObjectResult(new ApiResponse(HttpStatusCode.Unauthorized));
}
}
}
}
This attribute takes an array of strings, which was needed in my case. I needed to pass different users roles to this attribute and return result based on some custom logic.
public class CustomAuthFilter : AuthorizeAttribute, IAuthorizationFilter
{
public CustomAuthFilter(params string[] args)
{
Args = args;
}
public string[] Args { get; }
public void OnAuthorization(AuthorizationFilterContext context)
{
//Custom code ...
//Resolving a custom Services from the container
var service = context.HttpContext.RequestServices.GetRequiredService<ISample>();
string name = service.GetName(); // returns "anish"
//Return based on logic
context.Result = new UnauthorizedResult();
}
}
You can decorate your controller with this attribute as shown below
[CustomAuthFilter("Anish","jiya","sample")]
public async Task<IActionResult> Index()
Sample is a class that returns a hard coded string
public class Sample : ISample
{
public string GetName() => "anish";
}
services.AddScoped(); //Register ISample, Sample as scoped.
FOR ASYNCHRONOUS SUPPORT use IAsyncAuthorizationFilter
public class CustomAuthFilter : AuthorizeAttribute, IAsyncAuthorizationFilter
{
public CustomAuthFilter(params string[] args)
{
Args = args;
}
public string[] Args { get; }
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
//DO Whatever...
//Resolve Services from the container
var service = context.HttpContext.RequestServices.GetRequiredService<ISample>();
var httpClientFactory = context.HttpContext.RequestServices.GetRequiredService<IHttpClientFactory>();
string name = service.GetName();
using var httpClient = httpClientFactory.CreateClient();
var resp = await httpClient.GetAsync("https://jsonplaceholder.typicode.com/todos/1");
var data = await resp.Content.ReadAsStringAsync();
//Return based on logic
context.Result = new UnauthorizedResult();
}
}
Hope that helps..

How to add multiple policies in action using Authorize attribute using identity 2.0?

I am identity 2.1.2 with asp.net core 2.0, I have application claim table which have claim type and claim value
i.e Assets ,Assets Edit,Assets, Assets View, where claim types are same with distinct claim values and I am creating policies using claim type name which is working fine for me no clue about how to add multiple policies in one action. Below code is being used in startup file to create policies.
services.AddAuthorization(options =>
{
var dbContext = SqlServerDbContextOptionsExtensions.UseSqlServer(new DbContextOptionsBuilder<MyDBContext>(),
Configuration.GetConnectionString("TestIdentityClaimAuth")).Options;
var dbCon = new MyDBContext(dbContext);
//Getting the list of application claims.
var applicationClaims = dbCon.ApplicationClaims.ToList();
var strClaimValues = string.Empty;
List<ClaimVM> lstClaimTypeVM = new List<ClaimVM>();
IEnumerable<string> lstClaimValueVM = null;// new IEnumerable<string>();
lstClaimTypeVM = (from dbAppClaim
in dbCon.ApplicationClaims
select new ClaimVM
{
ClaimType = dbAppClaim.ClaimType
}).Distinct().ToList();
foreach (ClaimVM objClaimType in lstClaimTypeVM)
{
lstClaimValueVM = (from dbClaimValues in dbCon.ApplicationClaims
where dbClaimValues.ClaimType == objClaimType.ClaimType
select dbClaimValues.ClaimValue).ToList();
options.AddPolicy(objClaimType.ClaimType, policy => policy.RequireClaim(objClaimType.ClaimType, lstClaimValueVM));
lstClaimValueVM = null;
}
});
And in my controller using the Autherize attribute like this.
[Authorize(Policy = "Assets Edit")]
Please shade some light on it thanks in advance.
For multiple policys, you could implement your own AuthorizeAttribute.
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;
}
}
}
only require one of the policy
[MultiplePolicysAuthorize("Assets View;Assets Edit;Assets Delete")]
only require all the policys
[MultiplePolicysAuthorize("Assets View;Assets Edit;Assets Delete", true)]
If you simply want to apply multiple policies, you can do this:
[Authorize(Policy = "Asset")]
[Authorize(Policy = "Edit")]
public class MyController : Controller {
}
EDIT: to clarify, this is additive - you must pass both policy requirements.
You can use make multiple requirements class implementing IAuthorizationRequirement, and register to the DI container the multiple requirements handlers of AuthorizationHandler.
So you can simply add them to your Policy using the AddRequirement inside AuthorizationPolicyBuilder
public AuthorizationPolicyBuilder AddRequirements(params IAuthorizationRequirement[] requirements);
Startup.cs:
services.AddScoped<IAuthorizationHandler, FooHandler>();
services.AddScoped<IAuthorizationHandler, BooHandler>();
services.AddAuthorization(authorizationOptions =>
{
authorizationOptions.AddPolicy(
"FooAndBooPolicy",
policyBuilder =>
{
policyBuilder.RequireAuthenticatedUser();
policyBuilder.AddRequirements(new FooRequirement(), new BooRequirement());
});
});
Requirements.cs:
public class FooRequirement : IAuthorizationRequirement { }
public class FooHandler : AuthorizationHandler<FooRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationContext context, FooRequirement requirement)
{
if (context.User.HasClaim(c => c.Type == "Foo" && c.Value == true))
{
context.Succeed(requirement);
return Task.FromResult(0);
}
}
}
public class BooRequirement : IAuthorizationRequirement { }
public class BooHandler : AuthorizationHandler<BooRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationContext context, BooRequirement requirement)
{
if (context.User.HasClaim(c => c.Type == "Boo" && c.Value == true))
{
context.Succeed(requirement);
return Task.FromResult(0);
}
}
}

Generate Swashbuckle API documentation based on roles / api_key

I’m using Swashbuckle (5.3.2) and it generates a nice API documentation.
To clarify my problem, I set up a small example project with no real meaning.
The API can only be used with a valid API key.
For that I introduced an ApiKeyFilter which validates the api_key and read out corresponding roles.
ApiKeyFilter
public class ApiKeyFilter : IAuthenticationFilter
{
private static Dictionary<string, String[]> allowedApps = new Dictionary<string, String[]>();
private readonly string authenticationScheme = "Bearer";
private readonly string queryStringApiKey = "api_key";
public bool AllowMultiple
{
get { return false; }
}
public ApiKeyFilter()
{
if (allowedApps.Count == 0)
{
allowedApps.Add("PetLover_api_key", new []{"PetLover"});
allowedApps.Add("CarOwner_api_key", new []{"CarOwner"});
allowedApps.Add("Admin_api_key", new []{"PetLover","CarOwner"});
}
}
public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
var req = context.Request;
Dictionary<string, string> queryStrings = req.GetQueryNameValuePairs().ToDictionary(x => x.Key.ToLower(), x => x.Value);
string rawAuthzHeader = null;
if (queryStrings.ContainsKey(queryStringApiKey))
{
rawAuthzHeader = queryStrings[queryStringApiKey];
}
else if (req.Headers.Authorization != null && authenticationScheme.Equals(req.Headers.Authorization.Scheme, StringComparison.OrdinalIgnoreCase))
{
rawAuthzHeader = req.Headers.Authorization.Parameter;
}
if (rawAuthzHeader != null && allowedApps.ContainsKey(rawAuthzHeader))
{
var currentPrincipal = new GenericPrincipal(new GenericIdentity(rawAuthzHeader), allowedApps[rawAuthzHeader]);
context.Principal = currentPrincipal;
}
else
{
context.ErrorResult = new UnauthorizedResult(new AuthenticationHeaderValue[0], context.Request);
}
return Task.FromResult(0);
}
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
context.Result = new ResultWithChallenge(context.Result);
return Task.FromResult(0);
}
}
public class ResultWithChallenge : IHttpActionResult
{
private readonly string authenticationScheme = "amx";
private readonly IHttpActionResult next;
public ResultWithChallenge(IHttpActionResult next)
{
this.next = next;
}
public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
var response = await next.ExecuteAsync(cancellationToken);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue(authenticationScheme));
}
return response;
}
}
The controller/resources can only be accessed if the requester has the corresponding role.
PetController
[Authorize(Roles = "PetLover")]
[RoutePrefix("api/pets")]
public class PetController : ApiController
{
// GET api/pet
[Route]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/pet/5
[Route("{id:int}")]
public string Get(int id)
{
return "value";
}
// POST api/pet
[Route]
public void Post([FromBody]string value)
{
}
// PUT api/pet/5
public void Put(int id, [FromBody]string value)
{
}
// DELETE api/pet/5
public void Delete(int id)
{
}
}
CarController
[RoutePrefix("api/cars")]
public class CarController : ApiController
{
// GET api/car
[AllowAnonymous]
[Route]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/car/5
[Authorize(Roles = "CarOwner")]
[Route("{id:int}")]
public string Get(int id)
{
return "value";
}
// POST api/car
[Authorize(Roles = "CarOwner")]
[Route]
public void Post([FromBody]string value)
{
}
}
WebApiConfig
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.Filters.Add(new ApiKeyFilter());
//config.MessageHandlers.Add(new CustomAuthenticationMessageHandler());
}
}
So far so good. No problem here.
Question:
Now I want that the ‘User’ roles are taken into account during the API generation. I only want to display the resources and actions in the documentation which the user can consume with this api_key.
The output should somehow look like (/swagger/ui/index?api_key=XXX):
Admin_api_key:
Car
get /api/cars
post /api/cars
get /api/cars/{id}
Pet
get /api/pets
post /api/pets
get /api/pets/{id}
CarOwner_api_key:
Car
get /api/cars
post /api/cars
get /api/cars/{id}
PetLover_api_key:
Car
get /api/cars
Pet
get /api/pets
post /api/pets
get /api/pets/{id}
invalid_api_key:
Nothing to display
I don’t have access to the HttpRequest during the API specification generation to read out any query string or any header information.
I already had a look into a DelegatingHandler but I have trouble to read out the Principal in any Swashbuckle filter (OperationFilter, DocumentFilter) and I’m also not able to read out the Principal in a CustomProvider.
public class CustomAuthenticationMessageHandler : DelegatingHandler
{
private static Dictionary<string, String[]> allowedApps = new Dictionary<string, String[]>();
private readonly string authenticationScheme = "Bearer";
private readonly string queryStringApiKey = "api_key";
public bool AllowMultiple
{
get { return false; }
}
public CustomAuthenticationMessageHandler()
{
if (allowedApps.Count == 0)
{
allowedApps.Add("PetLover_api_key", new[] {"PetLover"});
allowedApps.Add("CarOwner_api_key", new[] {"CarOwner"});
allowedApps.Add("Admin_api_key", new[] {"PetLover", "CarOwner"});
}
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var req = request;
Dictionary<string, string> queryStrings = req.GetQueryNameValuePairs().ToDictionary(x => x.Key.ToLower(), x => x.Value);
string rawAuthzHeader = null;
if (queryStrings.ContainsKey(queryStringApiKey))
{
rawAuthzHeader = queryStrings[queryStringApiKey];
}
else if (req.Headers.Authorization != null && authenticationScheme.Equals(req.Headers.Authorization.Scheme, StringComparison.OrdinalIgnoreCase))
{
rawAuthzHeader = req.Headers.Authorization.Parameter;
}
if (rawAuthzHeader != null && allowedApps.ContainsKey(rawAuthzHeader))
{
var currentPrincipal = new GenericPrincipal(new GenericIdentity(rawAuthzHeader), allowedApps[rawAuthzHeader]);
request.GetRequestContext().Principal = currentPrincipal;
}
else
{
}
return await base.SendAsync(request, cancellationToken);
}
}
I found some similar question/issues but no real answer.
Web API Documentation using swagger
Restrict access to certain API controllers in Swagger using Swashbuckle and ASP.NET Identity
{hxxps://}github.com/domaindrivendev/Swashbuckle/issues/334
{hxxps://}github.com/domaindrivendev/Swashbuckle/issues/735
{hxxps://}github.com/domaindrivendev/Swashbuckle/issues/478
I now found a solution which works for me.
I used the CustomAuthenticationMessageHandler (same as in my question) to put the rules into the HttpContext.
public class CustomAuthenticationMessageHandler : DelegatingHandler
{
private static Dictionary<string, String[]> allowedApps = new Dictionary<string, String[]>();
private readonly string authenticationScheme = "Bearer";
private readonly string queryStringApiKey = "api_key";
public bool AllowMultiple
{
get { return false; }
}
public CustomAuthenticationMessageHandler()
{
if (allowedApps.Count == 0)
{
allowedApps.Add("PetLover_api_key", new[] {"PetLover"});
allowedApps.Add("CarOwner_api_key", new[] {"CarOwner"});
allowedApps.Add("Admin_api_key", new[] {"PetLover", "CarOwner"});
}
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var req = request;
Dictionary<string, string> queryStrings = req.GetQueryNameValuePairs().ToDictionary(x => x.Key.ToLower(), x => x.Value);
string rawAuthzHeader = null;
if (queryStrings.ContainsKey(queryStringApiKey))
{
rawAuthzHeader = queryStrings[queryStringApiKey];
}
else if (req.Headers.Authorization != null && authenticationScheme.Equals(req.Headers.Authorization.Scheme, StringComparison.OrdinalIgnoreCase))
{
rawAuthzHeader = req.Headers.Authorization.Parameter;
}
if (rawAuthzHeader != null && allowedApps.ContainsKey(rawAuthzHeader))
{
var currentPrincipal = new GenericPrincipal(new GenericIdentity(rawAuthzHeader), allowedApps[rawAuthzHeader]);
request.GetRequestContext().Principal = currentPrincipal;
}
else
{
}
return await base.SendAsync(request, cancellationToken);
}
}
I introduced an custom Swashbuckle IDocumentFilter which reads out the rules from the HttpContext and remove the actions and resources from the SwaggerDocument which are not allowed for this rules (based on api_key).
public class AuthorizeRoleFilter : IDocumentFilter
{
public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer)
{
IPrincipal user = HttpContext.Current.User;
foreach (ApiDescription apiDescription in apiExplorer.ApiDescriptions)
{
var authorizeAttributes = apiDescription
.ActionDescriptor.GetCustomAttributes<AuthorizeAttribute>().ToList();
authorizeAttributes.AddRange(apiDescription
.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AuthorizeAttribute>());
if (!authorizeAttributes.Any())
continue;
var roles =
authorizeAttributes
.SelectMany(attr => attr.Roles.Split(','))
.Distinct()
.ToList();
if (!user.Identity.IsAuthenticated || !roles.Any(role => user.IsInRole(role) || role == ""))
{
string key = "/" + apiDescription.RelativePath;
PathItem pathItem = swaggerDoc.paths[key];
switch (apiDescription.HttpMethod.Method.ToLower())
{
case "get":
pathItem.get = null;
break;
case "put":
pathItem.put = null;
break;
case "post":
pathItem.post = null;
break;
case "delete":
pathItem.delete = null;
break;
case "options":
pathItem.options = null;
break;
case "head":
pathItem.head = null;
break;
case "patch":
pathItem.patch = null;
break;
}
if (pathItem.get == null &&
pathItem.put == null &&
pathItem.post == null &&
pathItem.delete == null &&
pathItem.options == null &&
pathItem.head == null &&
pathItem.patch == null)
{
swaggerDoc.paths.Remove(key);
}
}
}
swaggerDoc.paths = swaggerDoc.paths.Count == 0 ? null : swaggerDoc.paths;
swaggerDoc.definitions = swaggerDoc.paths == null ? null : swaggerDoc.definitions;
}
}
My WebApiConfig now looks like this. I removed my ApiKeyFilter because it is not needed anymore.
WebApiConfig
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
//config.Filters.Add(new ApiKeyFilter());
config.MessageHandlers.Add(new CustomAuthenticationMessageHandler());
}
}
SwaggerConfig
public class SwaggerConfig
{
public static void Register()
{
GlobalConfiguration.Configuration
.EnableSwagger(c =>
{
c.SingleApiVersion("v1", "SwashbuckleExample");
c.DocumentFilter<AuthorizeRoleFilter>();
})
.EnableSwaggerUi(c => { });
}
}
Additional
In my project I used a CustomProvider which cache the SwaggerDocument per api_key.
public class CachingSwaggerProvider : ISwaggerProvider
{
private static ConcurrentDictionary<string, SwaggerDocument> _cache =
new ConcurrentDictionary<string, SwaggerDocument>();
private readonly ISwaggerProvider _swaggerProvider;
public CachingSwaggerProvider(ISwaggerProvider swaggerProvider)
{
_swaggerProvider = swaggerProvider;
}
public SwaggerDocument GetSwagger(string rootUrl, string apiVersion)
{
HttpContext httpContext = HttpContext.Current;
string name = httpContext.User.Identity.Name;
var cacheKey = string.Format("{0}_{1}_{2}", rootUrl, apiVersion, name);
return _cache.GetOrAdd(cacheKey, (key) => _swaggerProvider.GetSwagger(rootUrl, apiVersion));
}
}

Categories