Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AddOpenIdConnect not properly parsing Token Claims from OIDC Providers #58989

Open
1 task done
QuinnBast opened this issue Nov 15, 2024 · 0 comments
Open
1 task done

Comments

@QuinnBast
Copy link

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

When using AddOpenIdConnect, the middleware is not properly parsing the access token's JWT Token Claims from OIDC Providers.
Specifically, I am testing this out with Keycloak, but I imagine this is similar with other providers too.

When using the .AddOpenIdConnect as an Authentication source, the integration works, however, once the access token is returned from the provider, the access token is not being properly parsed. It is able to parse some of the fields from the access token, but not others. Especially roles. For example, if the token is:

{
  "exp": 1720548651,
  "iat": 1720548351,
  "auth_time": 1720548351,
  "jti": "b9aca179-91c9-4528-834e-29f5d33e0308",
  "iss": "https://MyServer/realms/ATG",
  "aud": "account",
  "sub": "1ab9a926-d2a9-4941-9a01-ffe07d500abd",
  "typ": "Bearer",
  "azp": "ATG.ad.yaskawa.com",
  "sid": "fd0c995d-3ec7-4bec-8a0c-18449b393ee8",
  "acr": "1",
  "scope": "email profile",
  "email_verified": true,
  "roles": [
    "Admin",
    "view-profile"
  ],
  "name": "Eric Obermuller",
  "preferred_username": "[email protected]",
  "given_name": "Eric",
  "family_name": "O",
  "email": "[email protected]"
}

The resulting User in ASP.NET would only have the preferred_username name set. Some of the other fields were getting processed and put into the Claims object, like exp, iat, jti, sub, and sid (maybe some others) but many are missing. In the debugger, I only had 8 total Claims on the user. The user was missing a lot of the more detailed fields like roles, name, email, family_name, etc.

This made it impossible to lock down routes using [Authorize(Roles = "Admin")], as none of the user's roles were getting populated into their Claims, even if you set the options.TokenValidationParameters.RoleClaimType = "roles", it would not properly parse out the user's roles.

As a workaround for this, I had to add a custom event listener to the OnTokenValidated event and manually parsed out the Users roles and append them to the User's Identity:

options.Events.OnTokenValidated = async ctx =>
{
    // For some reason, the access token's claims are not getting added to the user in C#
    // So this method hooks into the TokenValidation and adds it manually...
    // This definitely seems like a bug to me.

    // First, let's just get the access token and read it as a JWT
    var token = ctx.TokenEndpointResponse.AccessToken;
    var handler = new JwtSecurityTokenHandler();
    var parsedJwt = handler.ReadJwtToken(token);
    
    // Once parsed, any `role` claims is just being set to the text "role" for the claim type.
    // But Microsoft requires using their enum, `ClaimTypes.Role` if you want to use the claims with the `[Authorize(Roles = "...")]` Annotation.
    // So, we need to convert any "role" claims in the JWT to the actual Microsoft enum for them to be properly picked up...
    // So convert them here...
    var updatedClaims = parsedJwt.Claims.ToList().Select(c =>
    {
        return c.Type == "role" ? new Claim(ClaimTypes.Role, c.Value) : c;
    });
    
    
    // Finally, use the new claims list that we grabbed from the JWT and add them as a new `Identity` that contains them.
    ctx.Principal.AddIdentity(new ClaimsIdentity(updatedClaims));
};

I am VERY certain that this is a bug with the Microsoft Authentication library, and this workaround should not be required from my understanding. See also this StackOverflow question

Expected Behavior

Expected the Token to be properly parsed without needing to add a custom event listener that manually parses and processes the token that is returned.

Steps To Reproduce

  • Deploy a keycloak instance
  • Create a User and a Client in Keycloak
  • Setup a C# Application using Authentication middleware:
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
    options.LoginPath = "/Account/Login";
})
.AddOpenIdConnect(options =>
{
    options.Authority = "https://MyServer/auth/realms/ATG";
    options.MetadataAddress = "https://MyServer/realms/ATG/.well-known/openid-configuration";
    options.ClientId = "ATG.ad.yaskawa.com";
    options.ClientSecret = "tpdyzbDOADYdsCUaoFz9bTJNqRsOsrcQ";
    options.ResponseType = "code";
    options.SaveTokens = true;
    
    options.Scope.Add("openid");
    options.CallbackPath = "/signin-oidc"; // Update callback path
    options.SignedOutCallbackPath = "/signout-callback-oidc"; // Update signout callback path
    options.TokenValidationParameters = new TokenValidationParameters
    {
        NameClaimType = "preferred_username",
        RoleClaimType = "roles"
    };
});

Add a protected route and verify that the user's claims are not getting processed.

    [Route("testAuth")]
    [HttpGet]
    [Authorize]
    public async Task<IActionResult> TestAuth()
    {  
        var token = await HttpContext.GetTokenAsync("access_token");
        var claims = User.Claims;
        foreach (var claim in claims)
        {
            Console.WriteLine($"{claim.Type}: {claim.Value}");
        }
        return Ok($"Authorized. {token}");
    }
  • Navigate to the route in your browser and login to keycloak
  • Copy the JWT and paste it into jwt.io
  • Verify that the Console.WriteLine that dumped the user's claims is missing Claims that are present in the access token and does not have any "role" claims, despite the JWT existing in the token.

Exceptions (if any)

No response

.NET Version

No response

Anything else?

<Project Sdk="Microsoft.NET.Sdk.Web">

    <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
        <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
        <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
        <PackageReference Include="System.DirectoryServices.Protocols" Version="9.0.0-rc.2.24473.5" />
        <PackageReference Include="System.Security.Cryptography.Algorithms" Version="4.3.1" />
    </ItemGroup>

</Project>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant