azure-activedirectory-identitymodel-extensions-for-dotnet icon indicating copy to clipboard operation
azure-activedirectory-identitymodel-extensions-for-dotnet copied to clipboard

Options to retrieve the entire JWT payload (or header) as a JsonElement

Open kevinchalet opened this issue 1 year ago • 10 comments

In OpenIddict, there are cases where I need to get the entire JWT payload as a JsonElement (always representing a JSON object by definition).

To achieve that, the current bits use the stringified EncodedPayload property:

using var document = JsonDocument.Parse(Base64UrlEncoder.Decode(token.EncodedPayload));
// ...

It works, but it's kinda inefficient as it requires re-parsing the payload, which is something JsonWebToken already does internally. With the move to System.Text.Json, is there now a better way to achieve that?

Thanks.

/cc @brentschmaltz @jennyf19

kevinchalet avatar Aug 25 '23 14:08 kevinchalet

@kevinchalet can you describe your scenario? When will you want the JsonElement.

brentschmaltz avatar Aug 28 '23 16:08 brentschmaltz

OpenIddict's server offers an opt-in authorization request caching feature that stores the actual payload as an encrypted JWT in a distributed cache and redirects the user agent to the authorization endpoint with just a request_id parameter attached, which allows working around large URI issues in delegated authentication scenarios. It basically works like request_uri, but the payload is stored by the server itself and not by the client.

For that, we need to preserve the exact type of each parameters (i.e we can't store/restore everything as string claims), so JsonElement is directly used.

Here's the code doing that:

using var document = JsonDocument.Parse(
    Base64UrlEncoder.Decode(((JsonWebToken) result.SecurityToken).InnerToken.EncodedPayload));
if (document.RootElement.ValueKind is not JsonValueKind.Object)
{
    throw new InvalidOperationException(SR.GetResourceString(SR.ID0117));
}

// Restore the request parameters from the serialized payload.
foreach (var parameter in document.RootElement.EnumerateObject())
{
    if (!context.Request.HasParameter(parameter.Name))
    {
        context.Request.AddParameter(parameter.Name, parameter.Value.Clone());
    }
}

If the raw JsonElement was exposed, it would avoid having to re-parse the JSON payload:

var element = ((JsonWebToken) result.SecurityToken).InnerToken.JsonElement;
if (element.ValueKind is not JsonValueKind.Object)
{
    throw new InvalidOperationException(SR.GetResourceString(SR.ID0117));
}

// Restore the request parameters from the serialized payload.
foreach (var parameter in element.EnumerateObject())
{
    if (!context.Request.HasParameter(parameter.Name))
    {
        context.Request.AddParameter(parameter.Name, parameter.Value.Clone());
    }
}

Hope it's clear 😄

kevinchalet avatar Aug 28 '23 23:08 kevinchalet

@kevinchalet You want to be able to obtain each property in the token as a JsonElement, to put them somewhere. Do you still the token deserialized? We may need to provide you with a hook where you can examine each property as we look through here: https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/9a5e3ae3c7136d815241ff4a82f345907d6240d4/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonWebToken.PayloadClaimSet.cs#L31

Another option would be for a user to provide a location to write the JsonElements.

brentschmaltz avatar Aug 29 '23 00:08 brentschmaltz

@kevinchalet You want to be able to obtain each property in the token as a JsonElement, to put them somewhere. Do you still the token deserialized?

Actually, for this scenario, I don't need the claims to be materialized as strongly typed CLR objects, I just need a JsonElement pointer that is used by OpenIddictParameter as a delayed accessor (of course, it's likely Wilson itself would need to materialize things like iss or iat as you said in the other thread) 😃

kevinchalet avatar Aug 29 '23 01:08 kevinchalet

Note: if it's too complicated or isn't a good fit for the new serialization model, it's not a huge deal, re-parsing the JWT payload is not the most efficient thing, but it works, so... 😄

kevinchalet avatar Aug 29 '23 01:08 kevinchalet

@kevinchalet we've decided not to take it right now, due to our tight deadlines. Probably post-GA.

jennyf19 avatar Aug 29 '23 16:08 jennyf19

@kevinchalet if you had a callback that had the utf8bytes, would you be able to use that?

brentschmaltz avatar Oct 10 '23 23:10 brentschmaltz

@kevinchalet we are going to be adding extensibility when reading the JsonWebToken, this feature might fix in. Any thoughts?

brentschmaltz avatar Feb 21 '24 22:02 brentschmaltz

@kevinchalet if you had a callback that had the utf8bytes, would you be able to use that?

Interesting, what would it look like concretely? If you have something ready to test, I'd love to give it a try 😃

kevinchalet avatar Feb 22 '24 08:02 kevinchalet

I'm thinking here: having a callback that passes the ReadOnlySpan. This will be the UTF8 chars that can be passed to a JsonDocument or Utf8JsonReader.

brentschmaltz avatar Feb 22 '24 18:02 brentschmaltz