Bmoe
Bmoe

Reputation: 978

JwtSecurityToken Expiry Time Invalid .NET Core 3.1

I upgraded my app from .NET Core 2.2 to .NET Core 3.1. When I tried to test out my endpoints with PostMan I noticed I was getting 401 Unauth Error. When I look at the header I am seeing that the expiry time is invalid:

Invalid Token

I took the following bearer token:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiQm9iIiwibmJmIjoiMTYxNzk3Nzg1MSIsImV4cCI6IjE2MjMxNjE4NTYiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOlsiQmFza2V0YmFsbCIsIlJ1Z2J5IiwiRm9vdGJhbGwiXX0.QRLuXFeopf7QZ1NUzWcctuSfnNXiPgc2UH7NxAuHYvw

that I am getting on my generate token endpoint and decoded it with jwt.io and the exp field is "1623161856". Decoded Token I converted that to a date object in Javascript and it equals 60 days in the future. Converted exp time

So token certainly is not expired. Not sure if I missing anything in the upgrade to .NET Core 3.1 but here is the relevant code:

In Startup.cs I have

   public void ConfigureServices(IServiceCollection services)
    {


        // Initial Setup
        services.AddMvc();
        services.AddSingleton<IConfiguration>(Configuration);
        // Call this in case you need aspnet-user-authtype/aspnet-user-identity
        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
        // Register the Swagger generator, defining one or more Swagger documents 
        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc(Configuration["v1"], new OpenApiInfo { Title = Configuration["Sports"], Version = Configuration["v1] });
        });

        services.AddDataProtection();
        
        //Authentication Setup
        services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateAudience = false,
                ValidateIssuer = false,
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("AnythingYouWant")),
                ValidateLifetime = true,
                ClockSkew = TimeSpan.FromMinutes(5)
            };
            options.SaveToken = true;
            options.Events = new JwtBearerEvents()
            {
                OnTokenValidated = context =>
                {
                    var accessToken = context.SecurityToken as JwtSecurityToken;
                    if (accessToken != null)
                    {
                        ClaimsIdentity identity = context.Principal.Identity as ClaimsIdentity;
                        if (identity != null)
                        {
                            identity.AddClaim(new Claim("access_token", accessToken.RawData));
                        }
                    }

                    return Task.CompletedTask;
                }
            };
        });
        services.AddAuthorization();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseBrowserLink();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseSwagger();

        app.UseSwaggerUI(c =>
        {
            c.SwaggerEndpoint("/swagger/" + Configuration["v1"] + "/swagger.json", Configuration["Sports"]);
        });
        
        app.UseStaticFiles();
        app.UseRouting();
        app.UseCors();

        app.UseAuthentication();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
            endpoints.MapControllerRoute("swagger", "swagger/");
        });

        app.UseWelcomePage("/swagger");
    }

And the token is generated by one of my api endpoints. That code is as shown:

 [HttpPost("SportApi/Token")]
    [ServiceFilter(typeof(SportResourceFilter))]
    public IActionResult Create(string key)
    {
       return new ObjectResult(GenerateToken(key));
    }
 private string GenerateToken(string someKey)
    {
        JwtSecurityToken token = new JwtSecurityToken();
        List<SportAPIKey> ro = new List<SportAPIKey>();

        if (!string.IsNullOrEmpty(someKey))
        {

            using (StreamReader r = new StreamReader("keys.json"))
            {
                string json = r.ReadToEnd();
                ro = JsonConvert.DeserializeObject<List<SportAPIKey>>(json);
            }
            
            if (ro.Exists(sak => sak.SportAPIKeyValue.Equals(someKey)))
            {
                SportAPIKey sportapikey = ro.Find(sak => sak.SportAPIKeyValue.Equals(someKey));
                List<Claim> lc = new List<Claim>();
                Claim claimClient = new Claim(ClaimTypes.Name, sportapikey.Client);
                lc.Add(claimClient);
                foreach (string team in sportapikey.Teams)
                    {
                        lc.Add(new Claim(ClaimTypes.System, team.Trim()));
                    }
                Claim claimEffDate = new Claim(JwtRegisteredClaimNames.Nbf, new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds().ToString());
                lc.Add(claimEffDate);
                int tokenLifespan = 60;
                Claim claimExpDate = new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(DateTime.Now.AddDays(tokenLifespan)).ToUnixTimeSeconds().ToString());
                lc.Add(claimExpDate);

                foreach (string sport in sportapikey.Sports.Split(","))
                {
                    lc.Add(new Claim(ClaimTypes.Role, sport.Trim()));
                }

                var claims = lc.ToArray();
                
                token = new JwtSecurityToken(
                    new JwtHeader(new SigningCredentials(
                        new SymmetricSecurityKey(Encoding.UTF8.GetBytes("AnythingYouWant")),
                                                 SecurityAlgorithms.HmacSha256)),
                    new JwtPayload(claims));
            }
        }

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

Upvotes: 2

Views: 1470

Answers (1)

jps
jps

Reputation: 22555

Your token contains the timestamps nbfand expas string:

  "nbf": "1617977851",
  "exp": "1623161856",

which is invalid. On https:\jwt.io you can see that something is wrong when you hover with your mouse over these values. Ususally it shows the timestamp, but in your example it shows: "Invalid date":

enter image description here

Timestamps in a JWT are supposed to be numerical values:

NumericDate
A JSON numeric value representing the number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time

The values itself are correct:

enter image description here

The error happens during generation of the claims:

Claim claimExpDate = new Claim(JwtRegisteredClaimNames.Exp, 
                               new DateTimeOffset(DateTime.Now.AddDays(tokenLifespan)).ToUnixTimeSeconds().ToString());
lc.Add(claimExpDate);

Instead use this constructor, which lets you set the ClaimValueType:

new Claim(JwtRegisteredClaimNames.Exp, 
          new DateTimeOffset(DateTime.Now.AddDays(tokenLifespan)).ToUnixTimeSeconds().ToString(), 
          ClaimValueTypes.Integer64)

Or even better, let the framework add the expiration timestamp for you, using this constructor:

public JwtSecurityToken (string issuer = default, string audience = default, System.Collections.Generic.IEnumerable<System.Security.Claims.Claim> claims = default, DateTime? notBefore = default, DateTime? expires = default, Microsoft.IdentityModel.Tokens.SigningCredentials signingCredentials = default);

e.g.:

JwtSecurityToken(expires: DateTime.UtcNow.AddDays(60),  ...)

Upvotes: 5

Related Questions