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

JwtSecurityTokenHandler token expiration date validation fails if date more than 25 years #63001

Closed
yoyrandao opened this issue Dec 19, 2021 · 10 comments
Labels
area-System.Security tracking-external-issue The issue is caused by external problem (e.g. OS) - nothing we can do to fix it directly

Comments

@yoyrandao
Copy link

yoyrandao commented Dec 19, 2021

Description

JwtSecurityTokenHandler.ValidateToken fails on expire date validation if it is more than 25 years.

Reproduces on .NET 6 with more than 25 years expiration dates. On .NET 5 fails around dates with 100 years more than now.

Reproduction Steps

Token issuing

	var tokenDescriptor = new SecurityTokenDescriptor
	{
		Subject = new ClaimsIdentity(new Claim[]
		{
			new(ClaimTypes.NameIdentifier, /* some data */)
		},
		Expires = DateTime.UtcNow.AddYears(26 /* or other number more than 25 years on .NET 6 */),
                /* signing key, issuer, etc */
	};

Token validation

identity = tokenHandler.ValidateToken(jwtToken, new TokenValidationParameters
{
	ValidateAudience = false,
	ValidateIssuer = true,
	ValidateLifetime = true,
	ValidateIssuerSigningKey = true,
	IssuerSigningKey = new SymmetricSecurityKey(signingKey)
}, out _);

Issued token (from jwt.io)

image

Expected behavior

Validation must be successful.

Actual behavior

Validation code above throws SecurityTokenNoExpirationException with message IDX10225: Lifetime validation failed. The token is missing an Expiration Time. Tokentype: 'System.String'..

Regression?

No response

Known Workarounds

No response

Configuration

SDK: .NET 6 (but bug can be reproduced on .NET 5 and further)
OS: Windows 11 21H2 x64 (22000.376)

Other information

No response

@dotnet-issue-labeler dotnet-issue-labeler bot added area-System.Security untriaged New issue has not been triaged by the area owner labels Dec 19, 2021
@ghost
Copy link

ghost commented Dec 19, 2021

Tagging subscribers to this area: @dotnet/area-system-security, @vcsjones, @krwq
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

JwtSecurityTokenHandler.Validate token fails on expire date validation if it is more than 25 years.

Reproduces on .NET 6 with more than 25 years expiration dates. On .NET 5 fails around dates with 100 years more than now.

Reproduction Steps

Token issuing

	var tokenDescriptor = new SecurityTokenDescriptor
	{
		Subject = new ClaimsIdentity(new Claim[]
		{
			new(ClaimTypes.NameIdentifier, /* some data */)
		},
		Expires = DateTime.UtcNow.AddYears(26 /* or other number more than 25 years on .NET 6 */),
                /* signing key, issuer, etc */
	};

Token validation

identity = tokenHandler.ValidateToken(jwtToken, new TokenValidationParameters
{
	ValidateAudience = false,
	ValidateIssuer = true,
	ValidateLifetime = true,
	ValidateIssuerSigningKey = true,
	IssuerSigningKey = new SymmetricSecurityKey(signingKey)
}, out _);

Issued token (from jwt.io)

image

Expected behavior

Validation must be successful.

Actual behavior

Validation code above throws SecurityTokenNoExpirationException with message IDX10225: Lifetime validation failed. The token is missing an Expiration Time. Tokentype: 'System.String'..

Regression?

No response

Known Workarounds

No response

Configuration

SDK: .NET 6 (but bug can be reproduced on .NET 5 and further)
OS: Windows 11 21H2 x64 (22000.376)

Other information

No response

Author: yoyrandao
Assignees: -
Labels:

area-System.Security, untriaged

Milestone: -

@danmoseley
Copy link
Member

@vcsjones
Copy link
Member

vcsjones commented Dec 21, 2021

I'm unable to reproduce this. Here is a fully running example:

using System;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.JsonWebTokens;

static class Program {
    static void Main()
    {
        byte[] signingKey = new byte[32];
        SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor {
            Subject = new ClaimsIdentity(new Claim[] {
                new Claim(ClaimTypes.NameIdentifier, "Kevin")
            }),
            Expires = DateTime.UtcNow.AddYears(100),
            Issuer = "vcsjones",
            SigningCredentials = new SigningCredentials(
                new SymmetricSecurityKey(signingKey),
                SecurityAlgorithms.HmacSha256),
        };

        JsonWebTokenHandler handler = new();
        string token = handler.CreateToken(tokenDescriptor);

        Console.WriteLine(token);

        TokenValidationResult result = handler.ValidateToken(token,
            new TokenValidationParameters {
                ValidateAudience = false,
                ValidateIssuer = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuers = new string[] { "vcsjones" },
                IssuerSigningKey = new SymmetricSecurityKey(signingKey)
            }
        );

        Console.WriteLine(result.IsValid);
    }
}
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="6.15.0" />
  </ItemGroup>
</Project>

For me, this prints "true" at the end with an expiration 100 years in the future.

Can you share a complete code example that reproduces the issue, including the .csproj?

@ghost
Copy link

ghost commented Dec 21, 2021

This issue has been marked needs more info since it may be missing important information. Please refer to our contribution guidelines for tips on how to report issues effectively.

@yoyrandao
Copy link
Author

yoyrandao commented Dec 22, 2021

@vcsjones, my fault, i did not paste a token handler type 😅

I am using JwtSecurityTokenHandler, not JsonWebTokenHandler. Maybe, that is a kinda of solution.
Have you tried to reproduce this with JwtSecurityTokenHandler?

I've already send a complete part of code, so here is a csproj (most likely, you will not see anything strange here)

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

    <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <Deterministic>false</Deterministic>
        <GenerateAssemblyTitleAttribute>false</GenerateAssemblyTitleAttribute>
        <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
        <GenerateAssemblyVersionAttribute>false</GenerateAssemblyVersionAttribute>
        <GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
        <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
        <GenerateAssemblyInformationalVersionAttribute>false</GenerateAssemblyInformationalVersionAttribute>
        <RootNamespace>BlaBlaBla</RootNamespace>
        <AssemblyName>BlaBlaBla</AssemblyName>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <ItemGroup>
        <Compile Include="..\ProductSettings.cs" Link="Properties\ProductSettings.cs" />
    </ItemGroup>

    <ItemGroup>
        <PackageReference Include="Ensure.That" Version="10.1.0" />
        <PackageReference Include="FluentMigrator" Version="3.3.1" />
        <PackageReference Include="FluentMigrator.Runner" Version="3.3.1" />
        <PackageReference Include="FluentMigrator.Runner.Postgres" Version="3.3.1" />
        <PackageReference Include="linq2db" Version="4.0.0-preview.8" />
        <PackageReference Include="linq2db.PostgreSQL" Version="4.0.0-preview.8" />
        <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
        <PackageReference Include="MongoDB.Driver" Version="2.13.1" />
        <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
        <PackageReference Include="Npgsql" Version="5.0.10" />
        <PackageReference Include="Serilog" Version="2.10.0" />
        <PackageReference Include="YamlDotNet" Version="11.2.1" />
    </ItemGroup>

</Project>

@ghost ghost added needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration and removed needs more info labels Dec 22, 2021
@vcsjones
Copy link
Member

@yoyrandao Thanks for the update. I can reproduce the behavior, but I don't see a difference between .NET 5 and .NET 6.

Reproduces on .NET 6 with more than 25 years expiration dates. On .NET 5 fails around dates with 100 years more than now.

Given that, I would expect 75 years to fail on .NET 6, and work on .NET 5. However when I try it, it fails for both:

image

Based on your description, it sounds like this is failing right around the Unix Epoch 32-bit overflow, the Year 2038 problem. Indeed, this seems to be the case. This will work:

DateTime unixEpoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
byte[] signingKey = new byte[32];
SecurityTokenDescriptor tokenDescriptor = new() {
    Subject = new ClaimsIdentity(new Claim[] {
        new Claim(ClaimTypes.NameIdentifier, "Kevin")
    }),
    Expires = unixEpoch.AddSeconds(int.MaxValue),
    Issuer = "vcsjones",
    SigningCredentials = new SigningCredentials(
        new SymmetricSecurityKey(signingKey),
        SecurityAlgorithms.HmacSha256),
};

JsonWebTokenHandler handler = new();
string token = handler.CreateToken(tokenDescriptor);

and this won't:

DateTime unixEpoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
byte[] signingKey = new byte[32];
SecurityTokenDescriptor tokenDescriptor = new() {
    Subject = new ClaimsIdentity(new Claim[] {
        new Claim(ClaimTypes.NameIdentifier, "Kevin")
    }),
    Expires = unixEpoch.AddSeconds(int.MaxValue).AddSeconds(1),
                                              // ^ Overflow unix epoch
    Issuer = "vcsjones",
    SigningCredentials = new SigningCredentials(
        new SymmetricSecurityKey(signingKey),
        SecurityAlgorithms.HmacSha256),
};

Essentially, JsonWebTokenHandler can't handle an exp larger than int.MaxValue.

I don't know enough about the internals of JsonWebTokenHandler to say exactly where the problem lies. You may be able to get better help in the https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet repo, where JsonWebTokenHandler lives.

Can you confirm my findings, that the behavior is not dependent on the .NET version? If you are still able to reproduce differing behavior between .NET 5 and .NET 6, can you adapt my example below to reproduce the issue?

Here is the complete code I used to reproduce my findings:

using System;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.JsonWebTokens;
using System.IdentityModel.Tokens.Jwt;

static class Program {


    static void Main()
    {
        DateTime unixEpoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        RunExample(unixEpoch.AddSeconds(int.MaxValue));
        Console.WriteLine("-------------");
        RunExample(unixEpoch.AddSeconds(int.MaxValue).AddSeconds(1));

        static void RunExample(DateTime expires)
        {
            Console.WriteLine($"Expires: {expires}");
            byte[] signingKey = new byte[32];
            SecurityTokenDescriptor tokenDescriptor = new() {
                Subject = new ClaimsIdentity(new Claim[] {
                    new Claim(ClaimTypes.NameIdentifier, "Kevin")
                }),
                Expires = expires,
                Issuer = "vcsjones",
                SigningCredentials = new SigningCredentials(
                    new SymmetricSecurityKey(signingKey),
                    SecurityAlgorithms.HmacSha256),
            };

            JsonWebTokenHandler handler = new();
            string token = handler.CreateToken(tokenDescriptor);

            JwtSecurityTokenHandler validationHandler = new();

            try
            {
                ClaimsPrincipal result = validationHandler.ValidateToken(token,
                    new TokenValidationParameters {
                        ValidateAudience = false,
                        ValidateIssuer = true,
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true,
                        ValidIssuers = new string[] { "vcsjones" },
                        IssuerSigningKey = new SymmetricSecurityKey(signingKey)
                    }, out SecurityToken securityToken
                );

                Console.WriteLine($"Token is valid and expires at {securityToken.ValidTo}");
            }
            catch
            {
                Console.WriteLine("Could not validate token.");
            }
        }
    }
}
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFrameworks>net5.0;net6.0</TargetFrameworks>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="6.15.0" />
    <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.15.0" />
  </ItemGroup>
</Project>

Run: dotnet run --framework net6.0, dotnet run --framework net5.0

@vcsjones vcsjones added needs more info and removed needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration labels Dec 22, 2021
@ghost
Copy link

ghost commented Dec 22, 2021

This issue has been marked needs more info since it may be missing important information. Please refer to our contribution guidelines for tips on how to report issues effectively.

@yoyrandao
Copy link
Author

@vcsjones yeah, I can confirm. I test it again ang get the same result. Thanks for helping me, my solution is using JsonWebTokenHandler instead of JwtSecurityTokenHandler. Thanks a lot!

@ghost ghost added needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration and removed needs more info labels Dec 22, 2021
@vcsjones
Copy link
Member

Looks like this has previously been reported here: AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet#92

@bartonjs bartonjs added tracking-external-issue The issue is caused by external problem (e.g. OS) - nothing we can do to fix it directly and removed untriaged New issue has not been triaged by the area owner needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration labels Jan 7, 2022
@bartonjs
Copy link
Member

bartonjs commented Jan 7, 2022

Since the class with the issue is in a different repository, and they already have an issue for it, closing this one.

@bartonjs bartonjs closed this as completed Jan 7, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Feb 6, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Security tracking-external-issue The issue is caused by external problem (e.g. OS) - nothing we can do to fix it directly
Projects
None yet
Development

No branches or pull requests

4 participants