Search code examples
c#oauth-2.0.net-coreclaims-based-identity

.Net Core OAuth Nested Claims are not mapped back to user


I'm trying to gain access to the claims after OAuth:

services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = COINBASE_AUTH_ID;
})
.AddCookie()
.AddOAuth(COINBASE_AUTH_ID, options =>
{
    options.ClientId = Configuration["Coinbase:ClientId"];
    options.ClientSecret = Configuration["Coinbase:ClientSecret"];
    options.CallbackPath = new PathString("/signin-coinbase");

    options.AuthorizationEndpoint = "https://www.coinbase.com/oauth/authorize";
    options.TokenEndpoint = "https://api.coinbase.com/oauth/token";
    options.UserInformationEndpoint = "https://api.coinbase.com/v2/user";

    options.SaveTokens = true;

    options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
    options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
    options.ClaimActions.MapJsonKey("urn:coinbase:avatar", "avatar_url");

    options.Events = new OAuthEvents
    {
        OnCreatingTicket = async context =>
        {
            var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
            request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
            request.Headers.Add("CB-VERSION", DateTime.Now.ToShortDateString()); 
            var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
            response.EnsureSuccessStatusCode();

            var user = JObject.Parse(await response.Content.ReadAsStringAsync());

            context.RunClaimActions(user);
        }
    };
});

To my understanding after obtaining the access token in the OnCreatingTicket I obtain a user through the UserEndpoint. When I call context.RunClaimActions(user) this should map the claims I've set in my options to my user.

In My Index model I try to obtain the mapped Claims like so:

public class IndexModel : PageModel
{
    public string CoinbaseId { get; set; }

    public string CoinbaseAvatar { get; set; }

    public string CoinbaseName { get; set; }

    public async Task OnGetAsync()
    {
        if (User.Identity.IsAuthenticated)
        {
            CoinbaseName = User.FindFirst(c => c.Type == ClaimTypes.Name)?.Value;
            CoinbaseId = User.FindFirst(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
            CoinbaseAvatar = User.FindFirst(c => c.Type == "urn:coinbase:avatar")?.Value;

        }
    }

Yet the model returns all null values. My response from coinbase contains all 3 of those claims, yet they don't seem to be mapped to my in memory user, does anyone know why?

EDIT

It turns out Coinbase doesn't return the user object directly it returns the user object wrapped in an object named Data.

I've tried prefixing my map with

options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "data.id"); 

I think I'll need to parse the data object manually


Solution

  • To fix this I just grabbed the nested property in the JObject with SelectToken

    var userData = JObject.Parse(await response.Content.ReadAsStringAsync());
    var user = userData["data"];
    
    context.RunClaimActions(JObject.FromObject(user));