Hot Chocolate Authenticated Connected User

Viewed 860

I have a very simple IQueryable in my Query class for GraphQL. What I am wanting to accomplish is to get the Authorized User that is requesting the chat information. Change context.Chats to context.Chats.Where(i => i.UserId == RequestingId). So you can't obtain private chats from my API. If you aren't authorized then you will always receive a empty list. I have seen how to do this with a MVC controller but that isn't the desired result.

public class Query
{
    [UseDbContext(typeof(AppDbContext))]
    [UseFiltering]
    [UseSorting]
    public IQueryable<Chat> GetChats([ScopedService] AppDbContext context)
    {
        return context.Chats
           .Include(m => m.Messages)
           .Include(r => r.Recipients);
    }
}
2 Answers

You can inject the ClaimsPrincipal of the User like the following:

public IQueryable<Chat> GetChats([ScopedService] AppDbContext context,
    [GlobalState(nameof(ClaimsPrincipal))] ClaimsPrincipal claimsPrincipal)
{
    // ...
}

With the upcoming v11.3.1 you will also be able to simply write:

public IQueryable<Chat> GetChats([ScopedService] AppDbContext context, 
    ClaimsPrincipal claimsPrincipal)
{
    // ...
}

You can access the Id of the authenticated user like this:

var userId = claimsPrincipal.FindFirstValue(ClaimTypes.NameIdentifier);

Note: The claim might be different depending on how you've setup your authentication/authorization mechanism.

[UseDbContext(typeof(AppDbContext))]
    [UseFiltering]
    [UseSorting]
    public IQueryable<Chat> GetChats([ScopedService] AppDbContext context, [Service] IHttpContextAccessor httpContextAccessor)
    {
        ClaimsPrincipal authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(httpContextAccessor.HttpContext.Request.Headers["Authorization"]), Startup.JWT_AUTH_TYPE));
        int userId;
        int.TryParse(authenticatedUser.Claims.FirstOrDefault(i => i.Type.Equals(ClaimTypes.NameIdentifier))?.Value, out userId);
        return context.Chats
            .Include(m => m.Messages)
            .Include(r => r.Recipients)
            .Where(i => i.Recipients.Any(i => i.UserId == userId));
    }

Add this to StartUp.cs

services.AddHttpContextAccessor();

Plus using JwtParser

public static class JwtParser
{
    public static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
    {
        List<Claim> claims = new();
        string payload = jwt.Split('.')[1];
        byte[] jsonBytes = ParseBase64WithoutPadding(payload);
        Dictionary<string, object> keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
        ExtractRolesFromJwt(claims, keyValuePairs);
        claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())));
        return claims;
    }

    private static void ExtractRolesFromJwt(List<Claim> claims, Dictionary<string, object> keyValuePairs)
    {
        keyValuePairs.TryGetValue(ClaimTypes.Role, out object roles);
        if (roles is null) return;
        string[] parsedRoles = roles.ToString().Trim().TrimStart('[').TrimEnd(']').Split(',');
        if (parsedRoles.Length > 1)
        {
            foreach (string parsedRole in parsedRoles)
            {
                claims.Add(new Claim(ClaimTypes.Role, parsedRole.Trim('"')));
            }
        }
        else
        {
            claims.Add(new Claim(ClaimTypes.Role, parsedRoles[0]));
        }
        keyValuePairs.Remove(ClaimTypes.Role);
    }

    private static byte[] ParseBase64WithoutPadding(string base64)
    {
        switch (base64.Length % 4)
        {
            case 2:
                base64 += "==";
                break;
            case 3:
                base64 += "=";
                break;
        }
        return Convert.FromBase64String(base64);
    }
}

This is something that I got working. Is there a better solution for this or is this good enough?

Related