[SOLVED] U13: Combine member login with authorizat...
# help-with-umbraco
d
I have a use case in which the out-of-the-box roles for Members are not sufficient for my needs. In my case, members will be part of "channels" and in each channel, the member might have different permissions. I need to set up my permissions so that I can verify them on a per-channel basis. I imagine I might have an endpoint like this:
Copy code
POST: /api/{channel}/messages/create
So now I need to check if this member has the message.write permission for example, specifically for the channel that is passed in the URL. Preferably, I'd use as much out-of-the-box as I can, so just aspdotnet authorization policies or some custom class that allows me to add claims based on the incoming URL so I could just use
[UmbracoMemberAuthorize]
. Has anyone done something like this before and is willing to share?
I've been able to make something that somewhat works, though not quite how I want it. I made a
Claims transformer
that can transform the claims on every request like this:
Copy code
csharp
public class IntranetContextualClaimsTransformer(IHttpContextAccessor httpContextAccessor)
    : IClaimsTransformation
{
    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        var httpContext = httpContextAccessor.GetRequiredHttpContext();
        if (httpContext.Request.RouteValues.TryGetValue("group", out var groupValue)
            && string.Equals(groupValue?.ToString(), "1233", StringComparison.Ordinal))
        {
            var identity = (ClaimsIdentity)principal.Identity;
            identity.AddClaim(new Claim(ClaimTypes.Role, "resource.read"));
        }
        return Task.FromResult(principal);
    }
}
Then I can create an api endpoint like this:
Copy code
csharp
[ApiController]
[Route("/api/{group:int}/[controller]/[action]")]
[UmbracoMemberAuthorize]
[Authorize(Roles = "resource.read")]
public class IntranetTestApiController
    : UmbracoApiController
{
    public IActionResult Get([FromRoute] int group)
    {
        return Ok(group);
    }
}
Now my endpoint only gets called if I'm logged in as a member AND I have the role
resource.read
assigned (when I enter 1233 as group). However, if I enter 1234 as group for example, I expect to get a 403 forbidden response, but instead I get a 200OK with an empty body. If I can get this to return a 403 forbidden, then I'm pretty much there!
Also, if I'm not logged in as member, I get a redirect response?
Aha! I think I found something! So it turns out that member authentication uses the "default" authentication scheme for cookies
IdentityConstants.ApplicationScheme
. I also discovered that there is a
ConfigureMemberCookieOptions
class that configures the behaviour on cookie login. This class assigns some custom events to overwrite the default cookie behaviour, specifically for controllers that implement
UmbracoApiController
. And the custom behaviour is... to do nothing? Anyway, I want to have the default behaviour, because that already takes care of api calls from the browser using the
X-Requested-With
header. So my solution is to not use UmbracoApiController and instead just use
Controller
as the base class. Now if I call the api and I add
X-Requested-With: XMLHttpRequest
as a header, I get the expected response codes.
So final solution:
Copy code
csharp
public class IntranetContextualClaimsTransformer(IHttpContextAccessor httpContextAccessor)
        : IClaimsTransformation
{
    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        var httpContext = httpContextAccessor.GetRequiredHttpContext();
        if (httpContext.Request.RouteValues.TryGetValue("group", out var groupValue)
            && string.Equals(groupValue?.ToString(), "1233", StringComparison.Ordinal))
        {
            var identity = (ClaimsIdentity)principal.Identity;
            identity.AddClaim(new Claim(ClaimTypes.Role, "resource.read"));
        }
        return Task.FromResult(principal);
    }
}
And then the controller:
Copy code
csharp
[ApiController]
[Route("/api/{group:int}/[controller]/[action]")]
[UmbracoMemberAuthorize]
[Authorize(Roles = "resource.read")]
public class IntranetTestApiController
    : Controller
{
    public IActionResult Get([FromRoute] int group)
    {
        return Ok(group);
    }
}
I can even use custom authorization policies with this approach!
I'm noticing that
[UmbracoMemberAuthorize]
makes database calls that impact the performance of my application. With that attribute, my succesful requests take about 400ms whereas without it, it only takes 35ms. But that's probably also because the database connection is somewhat slow. I don't notice a difference in behaviour though, without the attribute, so I guess I don't need it?
What's cool though, is that unauthorized or forbidden requests respond in 5ms, that feels fast!
19 Views