I have implemented Okta authentication into my asp.net core application via this tutorial:
https://developer.okta.com/blog/2022/04/20/dotnet-6-web-api
This is perfect for my SPA to authenticate with my api. However, there are different user types for accessing my API and this information is passed in the claims. I am trying to create a filter to make it very easy to implement for each endpoint I want to give access to. I have created a general filter and it works great but I do not pass any parameters:
public class OktaAuthorizeFilter : IAuthorizationFilter
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly Okta.IJwtValidator _validationService;
public OktaAuthorizeFilter(IHttpContextAccessor httpContextAccessor, IJwtValidator validationService)
{
_httpContextAccessor = httpContextAccessor;
_validationService = validationService;
}
public async void OnAuthorization(AuthorizationFilterContext context)
{
var authToken = _httpContextAccessor.HttpContext!.Request.Headers["Authorization"].ToString();
if (String.IsNullOrEmpty(authToken))
{
context.Result = new UnauthorizedObjectResult(string.Empty);
return;
}
var validatedToken = await _validationService.ValidateToken(authToken.Split(" ")[1]);
if (validatedToken == null)
{
context.Result = new UnauthorizedObjectResult(string.Empty);
return;
}
}
}
public class OktaAuthorizeAttribute : TypeFilterAttribute
{
public OktaAuthorizeAttribute() : base(typeof(OktaAuthorizeFilter))
{
}
}
This works great as follows:
[HttpGet(Name = "GetUserAccount")]
[OktaAuthorizeAttribute]
[EnableRateLimiting("api")]
public IActionResult Get()
{
UserAccount TestAccount = new UserAccount() {InternalUserNumber = 3, EmailAddress = "test@mail.com"};
var json = _userAccountService.Read(TestAccount);
return Ok(json);
}
To differentiate my user types the Okta JWT token has a claim that lists the user types. A user could be Type1 and Type2 or just Type1, etc. So I was hoping to implement my filter similar to how ms graph works as follows:
[OktaAuthorizeAttribute(Type = new string[] { "Type1", "Type2" })]
This way if a user satisfies one of these options then it will work. I am not sure however how to pass parameters to the filters to make this work and then how can I use them? Is this level of authorization possible?
EDIT 3/28/24
I have updated my code to the following and it runs but unfortunately it runs after my controller runs so it doesnt restrict access. Not sure why!
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using ARMS_API.Okta;
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
namespace ARMS_API.Filters
{
public class OktaAuthorizationFilter : IAuthorizationFilter
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly string[]? _permissions;
private readonly Okta.IJwtValidator? _validationService;
public OktaAuthorizationFilter(IHttpContextAccessor httpContextAccessor, Okta.IJwtValidator validationService, string[] Permissions)
{
_httpContextAccessor = httpContextAccessor;
_validationService = validationService;
_permissions = Permissions;
}
public async void OnAuthorization(AuthorizationFilterContext context)
{
var authToken = context.HttpContext!.Request.Headers["Authorization"].ToString();
//ensure that acccess token has data
if (String.IsNullOrEmpty(authToken) || _validationService == null)
{
context.Result = new UnauthorizedObjectResult(string.Empty);
return;
}
var validatedToken = await _validationService.ValidateToken(authToken.Split(" ")[1]);
if (validatedToken == null)
{
context.Result = new UnauthorizedObjectResult(string.Empty);
return;
}
var RoleClaim = validatedToken.Claims.Where(claim => claim.Type.ToString() == "role").FirstOrDefault();
if(RoleClaim == null)
{
context.Result = new UnauthorizedObjectResult(string.Empty);
return;
}
var roles = RoleClaim.Value.Split(',');
if(roles.Intersect(_permissions!).ToArray().IsNullOrEmpty())
{
context.Result = new UnauthorizedObjectResult(string.Empty);
return;
}
}
}
public class OktaAuthorizationAttribute : TypeFilterAttribute
{
public OktaAuthorizationAttribute(params string[] Permissions) : base(typeof(OktaAuthorizationFilter))
{
Arguments = new object[] { Permissions };
}
}
}
And now the attribute shows like this above the controller:
[OktaAuthorization("usertype1", "usertype2")]
In order to utilize this approach to using a filter with parameters set by a attribute and filter you need to ensure you utilize the Arguments parameter as follows:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using APP.Okta;
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
namespace APP.Filters
{
public class OktaAuthorizationFilter : IAsyncAuthorizationFilter
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly string[]? _permissions;
private readonly Okta.IJwtValidator? _validationService;
public OktaAuthorizationFilter(IHttpContextAccessor httpContextAccessor, Okta.IJwtValidator validationService, string[] Permissions)
{
_httpContextAccessor = httpContextAccessor;
_validationService = validationService;
_permissions = Permissions;
}
public async void OnAuthorizationAsync(AuthorizationFilterContext context)
{
var authToken = context.HttpContext!.Request.Headers["Authorization"].ToString();
//ensure that acccess token has data
if (String.IsNullOrEmpty(authToken) || _validationService == null)
{
context.Result = new UnauthorizedObjectResult(string.Empty);
return;
}
var validatedToken = await _validationService.ValidateToken(authToken.Split(" ")[1]);
if (validatedToken == null)
{
context.Result = new UnauthorizedObjectResult(string.Empty);
return;
}
var RoleClaim = validatedToken.Claims.Where(claim => claim.Type.ToString() == "roles").FirstOrDefault();
if(RoleClaim == null)
{
context.Result = new UnauthorizedObjectResult(string.Empty);
return;
}
var roles = RoleClaim.Value.Split(',');
if(roles.Intersect(_permissions!).ToArray().IsNullOrEmpty())
{
context.Result = new UnauthorizedObjectResult(string.Empty);
return;
}
}
}
public class OktaAuthorizationAttribute : TypeFilterAttribute
{
public OktaAuthorizationAttribute(params string[] Permissions) : base(typeof(OktaAuthorizationFilter))
{
Arguments = new object[] { Permissions };
}
}
}
THen to utilize the variable as a parameter in the attribute tag do as follows:
[OktaAuthorization("usertype1", "usertype2")]