Integrating Okta via a Authorization Filter

53 Views Asked by At

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 = "[email protected]"};
        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")]
2

There are 2 best solutions below

0
Qiuzman On BEST ANSWER

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")]
6
J.Memisevic On

Few thing here need changes - you can resolve your dependencies form HttpContext.RequestServices with in the AuthorizationFilterContext, so no need to inject IHttpContextAccessor since you already have HttpContext.

You can pass parameters to your attribute like this :

public class OktaAuthorizeFilter : Attribute, IAuthorizationFilter
    {
        public string[] Items;
        private readonly Okta.IJwtValidator _validationService;

        public OktaAuthorizeFilter()
        {
        }

        public async void OnAuthorization(AuthorizationFilterContext context)
        {
            var authToken = context.HttpContext!.Request.Headers["Authorization"].ToString();

            if (String.IsNullOrEmpty(authToken))
            {
                context.Result = new UnauthorizedObjectResult(string.Empty);
                return;
            }
            _validationService = context.HttpContext.RequestServices.GetService<Okta.IJwtValidator>();
            var validatedToken = await _validationService.ValidateToken(authToken.Split(" ")[1]);

            if (validatedToken == null)
            {
                context.Result = new UnauthorizedObjectResult(string.Empty);
                return;
            }
        }
    }

Then you can use it :

[OktaAuthorizeAttribute(Items = ["Type1", "Type2"])]

Not sure why do you use the IAuthorizationFilter when the use of AuthorizationHandler is so flexible - maybe something to consider in the future. Docs