.net-8.0openiddict

OpenIddict - custom claims missing from access tokens after upgrade to .NET8


Scenario

I have been using OpenIddict for a while in an Asp.NET 6 server application that provides authentication and issues tokens that are then consumed by separate Asp.NET apps. Identities are managed by Microsoft identity, using local accounts and social providers (Google/Microsoft/...). All has been working completely fine on .NET6.

Problem

After upgrading to .NET8, all custom claims added in the Authorization controller are missing from the tokens. The tokens are still issued and consumed, but the custom claims are not there.

Details

  1. This is the OpenIddict setup:

services
    .AddOpenIddict()

    .AddCore(options =>
        {
            options.UseEntityFrameworkCore().UseDbContext<AuthorizationDbContext>();
            options.UseQuartz();
        })

    .AddServer(options =>
    {
        options.AllowClientCredentialsFlow();
        options.AllowAuthorizationCodeFlow().RequireProofKeyForCodeExchange();
        options.AllowRefreshTokenFlow();

        options.SetTokenEndpointUris("/connect/token");
        options.SetAuthorizationEndpointUris("/connect/authorize");
        options.SetUserinfoEndpointUris("/connect/userinfo");

        var certificatesSection = configuration.GetSection("Certificates");
        string encryptionCertificatePath = certificatesSection.GetValue<string>("EncryptionCertificatePath") ?? throw new Exception("Missing EncryptionCertificatePath in appsettings.json");
        string signingCertificatePath = certificatesSection.GetValue<string>("SigningCertificatePath") ?? throw new Exception("Missing SigningCertificatePath in appsettings.json");

        X509Certificate2 encryptionCertificate = new X509Certificate2(encryptionCertificatePath, "", X509KeyStorageFlags.EphemeralKeySet);
        X509Certificate2 signingCertificate = new X509Certificate2(signingCertificatePath, "", X509KeyStorageFlags.EphemeralKeySet);

        options
            .AddEncryptionCertificate(encryptionCertificate)
            .AddSigningCertificate(signingCertificate);

            options.RegisterScopes(allScopes);

            options.UseAspNetCore()
                .EnableTokenEndpointPassthrough()
                .EnableAuthorizationEndpointPassthrough()
                .EnableUserinfoEndpointPassthrough()
                .EnableStatusCodePagesIntegration();

            options.DisableAccessTokenEncryption(); //temporarily disabled token encryption to allow viewing the token content
    })

    .AddValidation(options =>
    {
        options.UseLocalServer();
        options.UseAspNetCore();
    });

  1. In the Authorization controller, I add custom claims ("name", "some claim", "email") to the token:

[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Authorize()
{
    var request = HttpContext.GetOpenIddictServerRequest() ??
        throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

    var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);

    if (result == null || !result.Succeeded
        || request.HasPrompt(OpenIddictConstants.Prompts.Login)
        || (request.MaxAge != null
                    && result.Properties?.IssuedUtc != null
                    && DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value)
                )
    )
    {
        if (request.HasPrompt(OpenIddictConstants.Prompts.None))
        {
            return Forbid(authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                properties: new AuthenticationProperties(new Dictionary<string, string?>
                {
                    [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.LoginRequired,
                    [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
                }));
        }

        var prompt = string.Join(" ", request.GetPrompts().Remove(Prompts.Login));
        var parameters = Request.HasFormContentType ?
                            Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() :
                            Request.Query.Where(parameter => parameter.Key != Parameters.Prompt).ToList();

        parameters.Add(KeyValuePair.Create(Parameters.Prompt, new StringValues(prompt)));

        return Challenge(
                         authenticationSchemes: IdentityConstants.ApplicationScheme,
                         properties: new AuthenticationProperties
                         {
                             RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
                         });

    }

    if (result.Principal.Identity?.Name == null)
    {
        throw new InvalidOperationException("Cannot retrieve claims principal");
    }

    // Create a new claims principal
    var claims = new List<Claim>
    {
            // 'subject' claim which is required
            new Claim(OpenIddictConstants.Claims.Subject, result.Principal.Identity.Name),

            // custom claims <---- THESE WORK FINE IN .NET6 BUT ARE MISSING IN .NET8
            new Claim(OpenIddictConstants.Claims.Name, result.Principal.Identity.Name)
                .SetDestinations(OpenIddictConstants.Destinations.IdentityToken, OpenIddictConstants.Destinations.AccessToken),
            new Claim( ClaimTypes.NameIdentifier, result.Principal.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value)
                .SetDestinations(OpenIddictConstants.Destinations.IdentityToken, OpenIddictConstants.Destinations.AccessToken),
            new Claim("some claim", "some value").SetDestinations(OpenIddictConstants.Destinations.AccessToken),
            new Claim(OpenIddictConstants.Claims.Email, "some@email").SetDestinations(OpenIddictConstants.Destinations.IdentityToken, OpenIddictConstants.Destinations.AccessToken)
    };

    var claimsIdentity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
    var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
    claimsPrincipal.SetScopes(request.GetScopes());
    return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}


  1. Running on .NET6 - Using Postman to authenticate and get the token, then viewing the token using jwt.io, I can see the content of the token and can see the "name", "some claim", "email" claims in the token. This is fine and expected:
{
  "alg": "RS256",
  "kid": "23D938149D0B1432C6DA198AE6BA90F88893046F",
  "x5t": "I9k4FJ0LFDLG2hmK5rqQ-IiTBG8",
  "typ": "at+jwt"
}

{
  "sub": "radek...........",
  "name": "radek...........",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "daa1daf9-...........",
  "some claim": "some value",
  "email": "some@email",
  "oi_prst": "postman",
  "oi_au_id": "1da899ef-...........",
  "client_id": "postman",
  "oi_tkn_id": "5f78b200-...........",
  "scope": "offline_access Pardubice_API api",
  "exp": 1737714786,
  "iss": "https://localhost:44326/",
  "iat": 1737711186
}

  1. Problem shown here - Running on .NET8 - Using Postman to authenticate and get the token, then viewing the token using jwt.io, I can see the content of the token, and the custom claims "name", "some claim", "email" are missing from the token:

{
  "alg": "RS256",
  "kid": "23D938149D0B1432C6DA198AE6BA90F88893046F",
  "x5t": "I9k4FJ0LFDLG2hmK5rqQ-IiTBG8",
  "typ": "at+jwt"
}

{
  "sub": "radek...........",
  "oi_prst": "postman",
  "oi_au_id": "7989d993-...........",
  "client_id": "postman",
  "oi_tkn_id": "08f32dab-...........",
  "scope": "offline_access Pardubice_API api",
  "iss": "https://localhost:44326/",
  "exp": 1737715346,
  "iat": 1737711746
}

No errors or warnings are logged by OpenIddict.

What I have tried: I've read dozens issues on GitHub and StackOverflow questions, but none of them seem to address this specific issue. The closest I've found seems to be related to a change in handling JWT in .NET8 (JsonWebToken vs JwtSecurityToken, UseSecurityTokenValidators), and in general about breaking changes related to JWT in .NET8. Other things read and tried:

Also tried upgrading from .NET6 to .NET7 - in .NET7 the custom claims are still present in the token, but in .NET8 they are missing.

Any suggestions or pointers would be greatly appreciated. What part of the system could be deleting the claims? Is it .NET8? Or a combination of OpenIddict and .NET8? Or something else? Could it be that my OpenIddict (3.1.1) is "too old" for .NET8? Thanks for any input.


Solution

  • I'll post what has worked for me, just for the case someone were using the same versions and facing similar issue.

    I switched the projects to .NET 8, and upgraded OpenIddict via versions 4.x and 5.x to version 6.0.0, using OpenIddict's author's instructions:

    All the steps were smooth and easy. Once done, the custom claims are back, all seem to be working fine.