Degusto
Degusto

Reputation: 291

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.

Upvotes: 6

Views: 466

Answers (1)

Maxim Zabolotskikh
Maxim Zabolotskikh

Reputation: 3367

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);
}

Upvotes: 1

Related Questions