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

.NET 8 behaves differently for JwtBearerOptions in AddJwtBearer #52075

Closed
1 task done
diegosasw opened this issue Nov 15, 2023 · 21 comments
Closed
1 task done

.NET 8 behaves differently for JwtBearerOptions in AddJwtBearer #52075

diegosasw opened this issue Nov 15, 2023 · 21 comments
Labels
area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. question Status: Resolved

Comments

@diegosasw
Copy link

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

Upgrading an api project with authorization around JWT Bearer tokens from .NET 7 to .NET 8 has some behaviour changes and I can't find any information about it, so I'm guessing it's a bug.

It seems the AddJwtBearer and the JwtBearerOptions behave differently now and there is no way to bypass Signature validation like before.

More specifically, the ValidateIssuerSigningKey set to false along with the TokenValidationParameters.SignatureValidator = (token, _) => new JwtSecurityToken(token); // mock to bypass validation does not work and produces a Unauthorized 401 error with invalid_signature.

Example

// let's assume
var validateSignature = false;
// ...
services.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme,
   options =>
   {
	 if (validateSignature)
	 {
	     options.Authority = jwtAuthOptions.Authority;
	 }
	options.RequireHttpsMetadata = false;
	options.MapInboundClaims = false;
	options.TokenValidationParameters =
	   new TokenValidationParameters
	   {
		RoleClaimType = "role",
		ValidateAudience = true,
		ValidAudience = "foo",
		ValidateIssuerSigningKey =validateSignature ,
		ValidateIssuer = true,
		ValidIssuer = "bar",
		ValidateLifetime = JwtAuthOptions.ValidateLifetime
	 };
	if (!validateSignature) // This used to be required, as simply indicating false in ValidateIssuerSigningKey was not enough
	{
		options.TokenValidationParameters.SignatureValidator = (token, _) => new JwtSecurityToken(token);
	}
   });

Expected Behavior

The expected behavior is not to produce a 401 with invalid signature error when explicitly configured ValidateIssuerSigningKey to false and provided a TokenValidationParameters.SignatureValidator = (token, _) => new JwtSecurityToken(token); without checking signature.

Steps To Reproduce

Add Bearer authentication scheme and try to bypass signature validation

Exceptions (if any)

No exceptions, just 401 due to invalid signature when it should be bypassing signature validation.

.NET Version

8.0.100

Anything else?

No response

@christallire
Copy link

christallire commented Nov 15, 2023

For those who fall into this problem:
It's undocumented breaking change. You must change it to Microsoft.Identity.JsonWebTokens.JsonWebToken
like TokenValidationParameters.SignatureValidator = (token, _) => new JsonWebToken(token)

The good thing is now you can remove the package "System.IdentityModel.Tokens.Jwt", Forever.

@Tratcher Tratcher added area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer and removed area-security labels Nov 15, 2023
@Tratcher
Copy link
Member

@jennyf19

@jinweijie
Copy link

jinweijie commented Nov 16, 2023

Hello Team,

I have the same issue, after upgrading from .Net 7 to .Net 8, the following code does not work as expected:

            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
            JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();

            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.Authority = appSettings.OidcAuthority;
                    options.RequireHttpsMetadata = false;

                    var tvp = new TokenValidationParameters
                    {
                        ValidateAudience = appSettings.ValidateAudience,
                        ValidateIssuer = appSettings.ValidateIssuer,
                        
                        NameClaimType = JwtClaimTypes.PreferredUserName,
                        RoleClaimType = JwtClaimTypesExt.Role
                    };
                    
                    if(!string.IsNullOrWhiteSpace(appSettings.ValidIssuer))
                        tvp.ValidIssuer = appSettings.ValidIssuer;

                    options.TokenValidationParameters = tvp;
                    
                    if(!string.IsNullOrWhiteSpace(appSettings.Audience))
                        options.Audience = appSettings.Audience;
                });
            
            return services;

Before I can get roles like var roles = User.Roles; in controller.

After upgrading, the roles is empty.

I use the following code to debug:

               options.Events = new JwtBearerEvents
                    {
                        OnTokenValidated = async context =>
                        {
                            var identity = context.Principal.Identity as ClaimsIdentity;
                            if (identity != null)
                            {
                                // List all claims for debugging purposes
                                foreach (var claim in identity.Claims)
                                {
                                    Console.WriteLine($"Claim: {claim.Type} - {claim.Value}");
                                }
                            }
                        }
                    };

With .Net 7 it's like:

net7

But with .Net 8 it's like:

net8

It seems RoleClaimType = JwtClaimTypesExt.Role JwtClaimTypesExt.Role->"role" didn't work.

Could you please check? thanks in advance.

@halter73
Copy link
Member

halter73 commented Nov 16, 2023

It's undocumented breaking change.

We did make a breaking change announcement for the switch from JwtSecurityToken and JwtTokenValidators to JsonWebToken and TokenHandler in .NET 8 preview 7. https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/8.0/securitytoken-events

We recommend updating your code to utilize the newer, more-optimized types. But if that's not possible, you can set JwtBearerOptions.UseSecurityTokenValidators = true which is the second option listed in the "Recommended action" section of the announcement.

If there's something that is no longer possible with the new types, please file an issue at https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet. If you have suggestions for how to improve the breaking change announcement, you can suggest edits at https://github.com/dotnet/docs/blob/main/docs/core/compatibility/aspnet-core/8.0/securitytoken-events.md

@jinweijie
Copy link

Hi @halter73 , thanks for the hint, after I adding options.UseSecurityTokenValidators = true; (change back to old mode), it works as before.

But I would like follow the new approach, could you let me know with the new approach, how to map the custom named claim like in my case?

Thanks in advance!

@jinweijie
Copy link

jinweijie commented Nov 17, 2023

I found the solution myself:
update

            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
            JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();

with

JsonWebTokenHandler.DefaultInboundClaimTypeMap.Clear();

and the options.UseSecurityTokenValidators = true; is not required anymore.

UPDATE: Please see my comments below, my issue can also be fixed by options.MapInboundClaims = false

@mkArtakMSFT mkArtakMSFT added question ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. labels Nov 17, 2023
@ghost ghost added the Status: Resolved label Nov 17, 2023
@ghost
Copy link

ghost commented Nov 18, 2023

This issue has been resolved and has not had any activity for 1 day. It will be closed for housekeeping purposes.

See our Issue Management Policies for more information.

@ghost ghost closed this as completed Nov 18, 2023
@igaobingbing
Copy link

igaobingbing commented Nov 22, 2023

After using JsonWebTokenHandler in. net8, when SaveSigninToken=true is set, BootstrapContext is always null
(ClaimsIdentity) user Identity) BootstrapContext is always null

image
image

@aodpi
Copy link

aodpi commented Nov 22, 2023

Seems that options.MapInboundClaims = false works for me.

@jinweijie
Copy link

Seems that options.MapInboundClaims = false works for me.

Thanks @aodpi , after I add options.MapInboundClaims = false, I can remove
the JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); I think it should be a better approach.

@aodpi
Copy link

aodpi commented Nov 25, 2023

@jinweijie Another solution I found is to change from JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); to JsonWebTokenHandler.DefaultInboundClaimTypeMap.Clear(); which also works perfectly.

I think it's related to this breaking change.

@aivsim
Copy link

aivsim commented Nov 29, 2023

We are having the same problem with BootstrapContext being null as @igaobingbing specified
var bootstrapContext = claimsPrincipal.Identities.First().BootstrapContext.ToString();

@igaobingbing maybe you have figured out something regarding this?

@bricejar
Copy link

bricejar commented Dec 4, 2023

Same issue here.
ValidateIssuerSigningKey = false produces a 401 when a 200 is expected.

@wmmihaa
Copy link

wmmihaa commented Jan 10, 2024

I'm adding this comment as it might help others....
We've migrated an application from .net6 => .net8. Users can generate JWT tokens within the application and use the token to authenticate when calling the API of the same application.
After upgrading the application, users could not be authenticated and the response was always "invalid token" (Unable to decode the payload).

The problem in our case was that the JwtSecurityToken was setting the JwtRegisteredClaimNames.Iat claim to DateTime.UtcNow.ToString() which worked fine in .Net 6, where as this has changed in .Net 8 which requires the Iat to be set using epoch format. After this change was applied all JWT tokens were evaluated correctly.

@yrandin
Copy link

yrandin commented Jan 23, 2024

I'm using the TokenValidationParameters.ValidIssuers option and after upgrading from .Net 7 to .Net 8, all token are rejected with the message:

server: Kestrel
www-authenticate: Bearer error="invalid_token",error_description="The signature key was not found" 

Here is my configuration:

builder.Services.AddAuthentication(
    options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    }).AddJwtBearer(
        options =>
        {
            // define the authority for the OpenID connect calls
            options.Authority = "OidcAuthority url";

            // define the audience to which this service belongs.
            options.Audience = "audience";

            options.TokenValidationParameters.ValidIssuers = ["issuer1", "issuer2"];
        });

I could make it work with the suggested "downgrade":

builder.Services.AddAuthentication(
    options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    }).AddJwtBearer(
        options =>
        {
            // define the authority for the OpenID connect calls
            options.Authority = "OidcAuthority url";

            // define the audience to which this service belongs.
            options.Audience = "audience";
            options.UseSecurityTokenValidators = true;

            options.TokenValidationParameters.ValidIssuers = ["issuer1", "issuer2"];
            options.TokenValidationParameters.SignatureValidator = (token, _) => new JwtSecurityToken(token); // mock to bypass validation
        });

However, I would prefer to use the .Net 8 new way but I have no clue how it can be configured to allow my 2 issuers.
Anyone has a clue on what I'm missing ?

@UseMuse
Copy link

UseMuse commented Mar 26, 2024

Hello everyone, I fixed the problem by adding
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.4.1" />

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

  <PropertyGroup Label="Globals">
    <SccProjectName>SAK</SccProjectName>
    <SccProvider>SAK</SccProvider>
    <SccAuxPath>SAK</SccAuxPath>
    <SccLocalPath>SAK</SccLocalPath>
  </PropertyGroup>

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
	<ItemGroup>
		<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.3" />
		<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.4.1" /><!--I fixed the problem by adding-->
		<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="7.4.1" />

@brendancolebf
Copy link

We did make a breaking change announcement for the switch from JwtSecurityToken and JwtTokenValidators to JsonWebToken and TokenHandler in .NET 8 preview 7. https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/8.0/securitytoken-events

Everything on this issue is leading to this comment, but I'm failing to see how it's related. In our case, we're not down-casting any properties to JwtSecurityToken, and our JwtBearerOptions are dead-simple:

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = builder.Configuration["IdentitySettings:Domain"];
        options.Audience = builder.Configuration["IdentitySettings:Audience"];
        
        //Added to resolve HTTP 401
        options.TokenValidationParameters.SignatureValidator = (token, _) => new JsonWebToken(token);
    });

I'd love some clarity on this breaking change, because the fix of TokenValidationParameters.SignatureValidator = (token, _) => new JsonWebToken(token) doesn't appear anywhere on the announcement, and I can't see why it's required on such a simple implementation.

@killerwife
Copy link

We did make a breaking change announcement for the switch from JwtSecurityToken and JwtTokenValidators to JsonWebToken and TokenHandler in .NET 8 preview 7. https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/8.0/securitytoken-events

Everything on this issue is leading to this comment, but I'm failing to see how it's related. In our case, we're not down-casting any properties to JwtSecurityToken, and our JwtBearerOptions are dead-simple:

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = builder.Configuration["IdentitySettings:Domain"];
        options.Audience = builder.Configuration["IdentitySettings:Audience"];
        
        //Added to resolve HTTP 401
        options.TokenValidationParameters.SignatureValidator = (token, _) => new JsonWebToken(token);
    });

I'd love some clarity on this breaking change, because the fix of TokenValidationParameters.SignatureValidator = (token, _) => new JsonWebToken(token) doesn't appear anywhere on the announcement, and I can't see why it's required on such a simple implementation.

I have the same situation, my code is as follows:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            // Clock skew compensates for server time drift
            ClockSkew = TimeSpan.FromMinutes(1),

            // In this case audience is null, so don't validate it. Allow any combination.
            ValidateAudience = false,
        };

        // Metadata address for middleware to be able validate JWT token automatically with well-known configuration
        options.MetadataAddress = wellKnownUrl;

        options.TokenValidationParameters.SignatureValidator = (token, _) => new JsonWebToken(token); // this was added now
    });

The error Bearer error="invalid_token", error_description="The signature key was not found" was resolved by it and I can not find any explanation why anywhere why this is needed.

@halter73
Copy link
Member

halter73 commented Apr 5, 2024

The error Bearer error="invalid_token", error_description="The signature key was not found" was resolved by it and I can not find any explanation why anywhere why this is needed.

With the latest IdentityModel packages, SignatureValidator = (token, _) => new JsonWebToken(token) should effectively be a no-op. I wonder if the problem is that you're using mismatched IdentityModel dependency versions. You can check this with dotnet list yourProject.csproj package --include-transitive as noted in AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet#2510 (comment).

If that's not it, feel free to open a new issue with a link to a full repro project hosted on GitHub either here or at https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/.

@brendancolebf
Copy link

I wonder if the problem is that you're using mismatched IdentityModel dependency versions. You can check this with dotnet list yourProject.csproj package --include-transitive as noted in AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet#2510 (comment).

Looks like this was it, but it's such an easy gotcha. We'd manually brought in System.IdentityModel.Tokens.Jwt 7.5.0 to resolve CVE-2024-21319, but Microsoft.AspNetCore.Authentication.JwtBearer 8.0.3 only brings in Microsoft.IdentityModel.Protocols.OpenIdConnect 7.1.2.

Updating Microsoft.IdentityModel.Protocols.OpenIdConnect to 7.5.1 resolves the chain and removes the need for the SignatureValidator assignment.

@Lorthirash
Copy link

Lorthirash commented Apr 19, 2024

It just doesn't work for me in .NET 8. Invalid_token when i try authenticate but why?

my jwt service:

{
public class JwtService : ITokenCreationService
{

    private readonly IMemoryCache _memoryCache;
    private readonly UserManager<User> _userManager;
    private readonly IOptionsMonitor<JwtTokensOptions> _jwtTokensOptionsMonitor;

    public JwtService(IMemoryCache memoryCache, UserManager<User> userManager, IOptionsMonitor<JwtTokensOptions> jwtTokensOptionsMonitor)
    {
        _memoryCache = memoryCache;
        _userManager = userManager;
        _jwtTokensOptionsMonitor = jwtTokensOptionsMonitor;
    }

    public async Task<AuthenticationResponse> CreateTokensAsync(User user)
    {
        return new AuthenticationResponse
        {
            AccessToken = await CreateAccessTokenAsync(user),
            RefreshToken = CreateRefreshToken(user)
        };
    }

    private JwtToken CreateRefreshToken(User user)
    {

        JwtTokenOptions refreshTokenOptions = _jwtTokensOptionsMonitor.CurrentValue.RefreshTokenOptions;

        var claims = new Claim[]
        {
        new Claim(JwtRegisteredClaimNames.Sub, user.Id)
        };

        JwtToken refreshToken = CreateToken(refreshTokenOptions, claims);

        _memoryCache.Set(refreshToken.Value, 0, refreshToken.Expiration);

        return refreshToken;
    }

    private async Task<JwtToken> CreateAccessTokenAsync(User user)
    {
        IList<string> roles = await _userManager.GetRolesAsync(user);
        JwtTokenOptions accessTokenOptions = _jwtTokensOptionsMonitor.CurrentValue.AccessTokenOptions;

        var claims = new Claim[]
        {
        new Claim(JwtRegisteredClaimNames.Sub, user.Id),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
        new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToString()),
        new Claim(ClaimTypes.NameIdentifier, user.Id),
        new Claim(ClaimTypes.Name, user.UserName ?? string.Empty),
        new Claim(ClaimTypes.Email, user.Email ?? string.Empty)
        }
        .Union(roles.Select(role => new Claim(ClaimTypes.Role, role)))
        .ToArray();

        return CreateToken(accessTokenOptions, claims);
    }

    private JwtToken CreateToken(JwtTokenOptions jwtTokenOptions, Claim[] claims)
    {
        var expiration = DateTime.UtcNow.AddMinutes(jwtTokenOptions.ExpirationMinutes);

        var signingCredentials = new SigningCredentials(
            new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtTokenOptions.Key)),
            SecurityAlgorithms.HmacSha256
        );

        var token = new JwtSecurityToken(
            issuer: jwtTokenOptions.Issuer,
            audience: jwtTokenOptions.Audience,
            claims: claims,
            expires: expiration,
            signingCredentials: signingCredentials
        );

        var tokenHandler = new JwtSecurityTokenHandler();

        return new JwtToken
        {
            Value = tokenHandler.WriteToken(token),
            Expiration = expiration
        };
    }

    public async Task<AuthenticationResponse> RenewTokensAsync(string refreshToken)
    {
        if (!_memoryCache.TryGetValue(refreshToken, out var _))
        {
            throw new JwtException($"Refresh token is missing: {refreshToken}");
        }

        JwtTokenOptions refreshTokenOptions = _jwtTokensOptionsMonitor.CurrentValue.RefreshTokenOptions;

        SecurityToken validatedToken;
        ClaimsPrincipal claimsPrincipal;
        var tokenHandler = new JwtSecurityTokenHandler();
        var tokenValidationParameters = new TokenValidationParameters
        {
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(refreshTokenOptions.Key)),
            ValidAudience = refreshTokenOptions.Audience,
            ValidIssuer = refreshTokenOptions.Issuer,
            ValidateLifetime = true,
            ValidateAudience = true,
            ValidateIssuer = true,
            ValidateIssuerSigningKey = true
        };

        try
        {
            claimsPrincipal = tokenHandler.ValidateToken(
                refreshToken, tokenValidationParameters, out validatedToken);
        }
        catch (SecurityTokenException exception)
        {
            throw new JwtException("JWT token validation failed.", exception);
        }

        User user = await _userManager.GetUserAsync(claimsPrincipal)
            ?? throw new InvalidOperationException(
                $"User not found with id {claimsPrincipal.FindFirstValue(ClaimTypes.NameIdentifier)}");

        return new AuthenticationResponse
        {
            AccessToken = await CreateAccessTokenAsync(user),
            RefreshToken = CreateRefreshToken(user)
        };
    }

    public void ClearRefreshToken(string refreshToken)
    {
        _memoryCache.Remove(refreshToken);
    }
}

}

program.cs:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters.SignatureValidator = (token, _) => new JsonWebToken(token);
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidAudience = builder.Configuration["JwtTokensOptions:AccessTokenOptions:Audience"],
ValidIssuer = builder.Configuration["JwtTokensOptions:AccessTokenOptions:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JwtTokensOptions:AccessTokenOptions:Key"] ?? string.Empty))
};

               });

This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer ✔️ Resolution: Answered Resolved because the question asked by the original author has been answered. question Status: Resolved
Projects
None yet
Development

No branches or pull requests