Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Resource based authorization in SignalR

I have web API with custom policies and authorization handlers. I wanted to reuse authorization handlers but HttpContext is null when attribute is used on signalr's hub.

For example this is my controller.

[Authorize]
public sealed class ChatsController : ControllerBase
{
    [HttpPost("{chatId}/messages/send")]
    [Authorize(Policy = PolicyNames.ChatParticipant)]
    public Task SendMessage() => Task.CompletedTask;
}

And this my my authorization handler. I can extract "chatId" from HttpContext and then use my custom logic to authorize user.

internal sealed class ChatParticipantRequirementHandler : AuthorizationHandler<ChatParticipantRequirement>
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ChatParticipantRequirementHandler(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ChatParticipantRequirement requirement)
    {
        if(_httpContextAccessor.HttpContext != null)
        {
            // Logic
        }

        return Task.CompletedTask;
    }
}

However this won't work with Azure SignalR because I don't have access to HttpContext. I know that I can provide custom IUserIdProvider but I have no idea how to access "chatId" from "Join" method in my custom authorization handler.

[Authorize]
public sealed class ChatHub : Hub<IChatClient>
{
    [Authorize(Policy = PolicyNames.ChatParticipant)]
    public async Task Join(Guid chatId)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, chatId.ToString());
}

Is it possible to reuse my authorization handlers? I would like to avoid copypasting my code. One solution is to extract my authorization code to separate services but then I have to manually call those from my hubs and abandon [Authorize] way.

like image 328
Degusto Avatar asked Jun 15 '21 15:06

Degusto


People also ask

How do you implement authentication in SignalR?

When using the browser client, no additional configuration is needed. If the user is logged in to your app, the SignalR connection automatically inherits this authentication. Cookies are a browser-specific way to send access tokens, but non-browser clients can send them.

Does SignalR require sticky session?

Sticky Sessions SignalR requires that all HTTP requests for a specific connection be handled by the same server process. When SignalR is running on a server farm (multiple servers), "sticky sessions" must be used. "Sticky sessions" are also called session affinity by some load balancers.


1 Answers

Your chat is a resource, and you want to use resource based authorization. In this case declarative authorization with an attribute is not enough, because chat id is known at runtime only. So you have to use imperative authorization with IAuthorizationService.

Now in your hub:

[Authorize]
public sealed class ChatHub : Hub<IChatClient>
{
    private readonly IAuthorizationService authService;

    public ChatHub(IAuthorizationService authService)
    {
        this.authService = authService;
    }

    public async Task Join(Guid chatId)
    {
        // Get claims principal from authorized hub context
        var user = this.Context.User;

        // Get chat from DB or wherever you store it, or optionally just pass the ID to the authorization service
        var chat = myDb.GetChatById(chatId);

        var validationResult = await this.authService.AuthorizeAsync(user, chat, PolicyNames.ChatParticipant);

        if (validationResult.Succeeded)
        {
            await Groups.AddToGroupAsync(Context.ConnectionId, chatId.ToString());
        }
    }
}

Your authorization handler should look different, because it needs the chat resource in its signature to do this kind of evaluation:

internal sealed class ChatParticipantRequirementHandler : AuthorizationHandler<ChatParticipantRequirement, Chat>
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ChatParticipantRequirementHandler(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ChatParticipantRequirement requirement, Chat chat)
    {
        // You have both user and chat now
        var user = context.User;
        if (this.IsMyUserAuthorizedToUseThisChat(user, chat))
        {
            context.Succeed(requirement);
        }
        else
        {
            context.Fail();
        }

        return Task.CompletedTask;
    }
}

Edit: there is actually another option I didn't know about

You can make use of HubInvocationContext that SignalR Hub provides for authorized methods. This can be automatically injected into your AuthorizationHandler, which should look like this:

public class ChatParticipantRequirementHandler : AuthorizationHandler<ChatParticipantRequirement, HubInvocationContext>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ChatParticipantRequirement requirement, HubInvocationContext hubContext)
        {
            var chatId = Guid.Parse((string)hubContext.HubMethodArguments[0]);
        }
    }

Hub method will be decorated normally with [Authorize(Policy = PolicyNames.ChatParticipant)]

You still will have two authorization handlers, AuthorizationHandler<ChatParticipantRequirement> and AuthorizationHandler<ChatParticipantRequirement, HubInvocationContext>, no way around it. As for code dublication, you can however just get the Chat ID in the handler, either from HttpContext or HubInvocationContext, and than pass it to you custom written MyAuthorizer that you could inject into both handlers:

public class MyAuthorizer : IMyAuthorizer 
{
  public bool CanUserChat(Guid userId, Guid chatId);
}
like image 63
Maxim Zabolotskikh Avatar answered Oct 01 '22 15:10

Maxim Zabolotskikh