Reputation: 1139
I have an authentication setup where I store a session ID and an unverified user ID in my claims. Then, in a normal controller, I would look up the session in my DB to verify that it matches the userID and consider the user logged in.
I am trying to use Azure SignalR. I want to be able to send messages to connected users by userID, and I need to implement IUserIdProvider. The GetUserId method on it isn't async, but what I need to do is perform the same logic, which verifies the session ID and userID in the claims against the database before it considers the user valid. This code would be async, but the GetUserId method isn't async.
What options do I have?
Thanks!
Upvotes: 1
Views: 970
Reputation: 387
I believe the reasoning behind IUserIdProvider.GetUserId(HubConnectionContext)
not being async is that calls to the DB or other external resource would occur earlier in the request pipeline. This could include operations like mapping an external user id or session id to an internal user id.
The way I solved a similar problem was to place my DB lookups in an IClaimsTransformation implementation which stored the values I looked up in Claims on the User that flows through the request.
public static class ApplicationClaimTypes
{
public static string UserId => "user-id";
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Identity.Web;
class MsalClaimsTransformation : IClaimsTransformation
{
// IMapExternalUsers represents the actions you must take to map an external id to an internal user id
private readonly IMapExternalUsers _externalUsers;
private ClaimsPrincipal _claimsPrincipal;
public MsalClaimsTransformation(IMapExternalUsers externalUsers)
{
_externalUsers = externalUsers;
}
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal claimsPrincipal)
{
_claimsPrincipal = claimsPrincipal;
// This check is important because the IClaimsTransformation may run multiple times in a single request
if (!claimsPrincipal.HasClaim(claim => claim.Type == ApplicationClaimTypes.UserId))
{
var claimsIdentity = await MapClaims();
claimsPrincipal.AddIdentity(claimsIdentity);
}
return claimsPrincipal;
}
private async Task<ClaimsIdentity> MapClaims()
{
var externalIds = new[]
{
// Extensions from Microsoft.Identity.Web
_claimsPrincipal.GetHomeObjectId(),
_claimsPrincipal.GetObjectId()
}
.Distinct()
.Where(id => !string.IsNullOrWhiteSpace(id))
.ToArray();
// Replace with implementation specific to your use case for mapping session/external
// id to authenticated internal user id.
var userId = await _externalUsers.MapExternalIdAsync(externalIds);
var claimsIdentity = new ClaimsIdentity();
AddUserIdClaim(claimsIdentity, userId);
// Add other claims as needed
return claimsIdentity;
}
private void AddUserIdClaim(ClaimsIdentity claimsIdentity, Guid? userId)
{
claimsIdentity.AddClaim(new Claim(ApplicationClaimTypes.UserId, userId.ToString()));
}
}
Once you have your internal user id stored in the User object as a claim it is accessible to SignalR's GetUserId()
method without the need for async code.
using System.Security.Claims;
using Microsoft.AspNetCore.SignalR;
internal class SignalrUserIdProvider : IUserIdProvider
{
public string GetUserId(HubConnectionContext connection)
{
var httpContext = connection.GetHttpContext();
// If you have a multi-tenant application, you may have an
// extension method like this to get the current tenant the user
// is in. Otherwise just remove it.
var tenantIdentifier = httpContext.GetTenantIdentifier();
var userId = connection.User.FindFirstValue(ApplicationClaimTypes.UserId);
if (string.IsNullOrWhiteSpace(userId))
{
return string.Empty;
}
return $"{tenantIdentifier}-{userId}";
}
}
Upvotes: 1