Search code examples
json.net-7.0claims

How to get a claim as JSON without the "Subject" field?


I use System.Security.Claims and JWT tokens in a basic Web API so there's an .AddAuthentication().AddJwtBearer() defined in my program.cs and some login method. This all works fine. But now I need a specific claim to be saved as JSON and returned to the client. This is a custom claim called "Host", which is just the host name where the request came on.
I use this code to basically convert any object into JSON:

public static class JSonHelpers
{
    public static JsonSerializerSettings NoLoop = new() { ReferenceLoopHandling = ReferenceLoopHandling.Ignore };
    
    public static string SaveAsJson<T>(this T data, Formatting format = Formatting.Indented, JsonSerializerSettings? settings = null)
    {
        try { return JsonConvert.SerializeObject(data, format, settings); }
        catch (Exception ex) { return ex.Message; }
    }
}

The code above basically gives every object an extra method to generate a JSON string from it's content. It also has some simple error handling as it will return the exception as string if something is wrong. Not pretty, but the client will handle any of these errors.

My controller has this method:

    [HttpGet("host")]
    [Authorize]
    [SwaggerResponse(StatusCodes.Status200OK)]
    [SwaggerResponse(StatusCodes.Status401Unauthorized)]
    [Produces(MediaTypeNames.Text.Plain)]
    public string HostClaim()
    {
        return User.Claims.Where(c=>c.Type=="Host").ToList().SaveAsJson(settings: JSonHelpers.NoLoop);
    }

Very simple method, actually. It returns an array because the 'where' filter will be adjusted in the future to return more than one claim.
You have to be authorized to call it, which means you also have the Host claim.
I now want the full claim returned without that "Subject" stuff.

Because I get this JSON content:

{
    "Issuer": "Katje",
    "OriginalIssuer": "Katje",
    "Properties": {},
    "Subject": {
      "AuthenticationType": "AuthenticationTypes.Federation",
      "IsAuthenticated": true,
      "Actor": null,
      "BootstrapContext": null,
      "Claims": [
        {
          "Issuer": "Katje",
          "OriginalIssuer": "Katje",
          "Properties": {},
          "Type": "User",
          "Value": "boss",
          "ValueType": "http://www.w3.org/2001/XMLSchema#string"
        },
        <<<< Lots more claims >>>>
        <<<< Lots more claims >>>>
        <<<< Lots more claims >>>>
        {             
          "Issuer": "Katje",
          "OriginalIssuer": "Katje",
          "Properties": {},
          "Type": "aud",
          "Value": "Everyone",
          "ValueType": "http://www.w3.org/2001/XMLSchema#string"
        }
      ],
      "Label": null,
      "Name": null,
      "NameClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
      "RoleClaimType": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
    },
    "Type": "Host",
    "Value": "localhost",
    "ValueType": "http://www.w3.org/2001/XMLSchema#string"
}

And this is way more than I need! This is because the "Subject" is pointing back to the list of all claims so I get all claims when I just need one. I could just create a custom anonymous class with all the fields except Subject. That's not very pretty either. But doing this:

return User.Claims.Where(c => c.Type == "Host").Select(r => new { r.Issuer, r.OriginalIssuer, r.Properties, r.Type, r.Value, r.ValueType }).ToList().SaveAsJson(settings: JSonHelpers.NoLoop);

That will work. But my problem is that I have to create a new, anonymous object and include the data I want.
Is it possible to instead exclude the field I don't want?


Solution

  • That will work. But my problem is that I have to create a new, anonymous object and include the data I want.

    That's the point when emitting data across boundaries. You create (anonymous) DTOs so you control what gets serialized and how.

    If you don't want that, you could clone the Claims instance, documented as:

    Returns a new Claim object copied from this object. The new claim does not have a subject.

    So:

    var hostClaim = User.Claims.Where(c => c.Type == "Host")
        .Select(c => c.Clone())
        .ToList()
        .SaveAsJson(settings: JSonHelpers.NoLoop);