ASP.NET Core 3.1 Web Api - tokeny JWT, exp date

0

Cześć,

kontynuując moje próby napisania sensownego WebApi natrafiłem na dziwną sytuację, w której, mimo, że token JWT generuję z expiration date na 1 minutę (testowo) to jest on ważny jakieś 4-5 minut. W samym tokenie czas podany jest prawidłowo. Przykładowy token:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIqIiwianRpIjoiMTI5ODJhNjItMzA0OS00ZDFmLThkNWItMTAwOWVhZDBlMGJhIiwiZXhwIjoxNTg2MDA2MjcxLCJpc3MiOiJDNUQzQTQ2MC1BRjVGLTMzRkYtOTYwMC0zMDA0NjA3NzRGRjkiLCJhdWQiOiI2MjFCNjJCMC04M0MyLUtLNTctQkVBRi05MzYyRDNEMzFFQzUifQ.rz3GVbz0gjVAVfVAH2IfdAUj-O0MSYncL8GFcIcnZjk

Skorzystałem do nauki z jakiegoś gotowego przykładu i nie mogę też znaleźć miejsca w kodzie, w którym ten token jest sprawdzany, nie rozumiem, jak to jest realizowane.
Dodatkowo zastanawiam się jak zrealizować coś takiego, że podczas weryfikacji tokena chciałbym się jeszcze połączyć z DB (nie wiem jak podpiąć się pod sprawdzanie tokena).

Poniżej troche kodu, uprzejma prośba o wsparcie ;)

UserIdentity.cs:

public class UserIdentity
    {
        [Required]
        [JsonProperty(Required = Required.DisallowNull)]
        public string Login { get; set; }

        [Required]
        [JsonProperty(Required = Required.DisallowNull)]
        public string Password { get; set; }
    }

JWTConfiguration.cs:

public class JWTConfiguration
    {
        public string SecretKey { get; set; }
        public string ValidIssuer { get; set; }
        public string ValidAudience { get; set; }
        public int TokenExpirationTime { get; set; }
    }

Startup:

public class Startup
    {
        public IConfigurationRoot Configuration { get; }

        public Startup(Microsoft.Extensions.Hosting.IHostEnvironment env)
        {
            var appSettings = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json")
                .Build();
            Configuration = appSettings;
        }
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddTransient((config) =>
            {
                var conf = new JWTConfiguration();
                Configuration.GetSection("JWTConfiguration").Bind(conf);
                return conf;
            });

            ConnectionStrings con = new ConnectionStrings();
            Configuration.Bind("ConnectionStrings", con);
            services.AddSingleton(con);

            services.AddControllers();
            services.AddMvc();

            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = ".Net Core 3 Web API - Tescik", Version = "v1" });
                var filePath = Path.Combine(AppContext.BaseDirectory, "NetCore3WebAPI.xml");
                c.IncludeXmlComments(filePath);
            });

            ConfigureJwt(services);
        }

        public void ConfigureJwt(IServiceCollection services)
        {
            var config = services.BuildServiceProvider().GetService<JWTConfiguration>();
            var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SecretKey));

            var tokenValidationParameters = new TokenValidationParameters
            {
                IssuerSigningKey = signingKey,
                ValidIssuer = config.ValidIssuer,
                ValidAudience = config.ValidAudience
            };

            services.AddAuthentication(o =>
            {
                o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(c =>
            {
                c.RequireHttpsMetadata = false;
                c.SaveToken = true;
                c.TokenValidationParameters = tokenValidationParameters;
            });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            
            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseCertificateForwarding();

            app.UseAuthentication();
           

            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
            app.UseSwagger();
            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "Testowe API (.NET Core 3,1 Web API) V1");
            });
        }
    }

Generowanie tokenów (AuthController):

[Route("api/[controller]")]
    [ApiController]
    public class AuthController : ControllerBase
    {
        private readonly ConnectionStrings con;
        private readonly UserIdentity userIdentity;
        private readonly JWTConfiguration configuration;

       
        public AuthController(ConnectionStrings c, JWTConfiguration configuration)
        {
            con = c;
            this.configuration = configuration;
        }

        [HttpPost]
        [Route("token")]
        public AccessToken GenerateToken([FromBody]UserIdentity credentials)
        {
            IEnumerable<User> user;
            using (var c = new MySqlConnection(con.MySQL))
            {
                string md5Password = string.Empty;
                using (MD5 md5Hash = MD5.Create())
                {
                    md5Password = GetMd5Hash(md5Hash, credentials.Password);
                }

                var sql = @$"select * from users where login = '{credentials.Login}' and password = '{md5Password}' ";
                
                var query = c.Query<User>(sql, User, commandTimeout: 3000);
                user = query;
            }

            if (user == null || user.Count() == 0)
            {
                Response.StatusCode = StatusCodes.Status401Unauthorized;
                return new AccessToken { Success = false };
            }

            var claims = new[]
            {
                new Claim(JwtRegisteredClaimNames.Sub, credentials.Login),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
            };

            var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(configuration.SecretKey));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var expiredOn = DateTime.Now.AddMinutes(configuration.TokenExpirationTime);
            var token = new JwtSecurityToken(configuration.ValidIssuer,
                  configuration.ValidAudience,
                  claims,
                  expires: expiredOn,
                  signingCredentials: creds);
            return new AccessToken
            {
                ExpireOnDate = token.ValidTo,
                Success = true,
                ExpiryIn = configuration.TokenExpirationTime,
                Token = new JwtSecurityTokenHandler().WriteToken(token)
            };
        }

        private static string GetMd5Hash(MD5 md5Hash, string input)
        {
            byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input));
            StringBuilder sBuilder = new StringBuilder();
            for (int i = 0; i < data.Length; i++)
            {
                sBuilder.Append(data[i].ToString("x2"));
            }
            return sBuilder.ToString();
        }
    }

I przykładowy endpoint wymagający autoryzacji:

[HttpGet]
       [Route("get/{userId}")]
       [Authorize]
        public async Task<IActionResult> Get(int userId)
        {
            return await Task.Run(() =>
            {
                using (var c = new MySqlConnection(con.MySQL))
                {
                    var sql = @"select * from users";
                    var query = c.Query<User>(sql, User , commandTimeout: 3000);
                    return Ok(query);
                }
               
            });
        }
1

nie rozumiem, jak to jest realizowane.

Masz gotowca w Startupie który przy [Authorize] wyciąga token z headera i wylicza czy token jest ok

public void ConfigureJwt(IServiceCollection services)
{
	var config = services.BuildServiceProvider().GetService<JWTConfiguration>();
	var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SecretKey));

	var tokenValidationParameters = new TokenValidationParameters
	{
		IssuerSigningKey = signingKey,
		ValidIssuer = config.ValidIssuer,
		ValidAudience = config.ValidAudience
	};

	services.AddAuthentication(o =>
	{
		o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
		o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
	})
	.AddJwtBearer(c =>
	{
		c.RequireHttpsMetadata = false;
		c.SaveToken = true;
		c.TokenValidationParameters = tokenValidationParameters;
	});
}
0

Wiem, że nie w tym problem, ale zwrócę tylko uwagę mimo że to testowy kod:

public AccessToken GenerateToken([FromBody]UserIdentity credentials)
{
      var sql = @$"select * from users where login = '{credentials.Login}' and password = '{md5Password}' ";
}

Aż się prosi o SQL Injection ;)

1

W ogóle czy jest sens robienia własnego hashowania haseł czy jest IPasswordHasher i gotowa implementacja?

https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.passwordhasher-1

no chyba, że ktoś nie chce zależności do identity

1

jeszcze 'odpowiedź' na drugie pytanie jak się podpiąć pod db w trakcie sprawdzania tokenu: https://stackoverflow.com/questions/41992845/loading-asp-net-core-authorization-policy-from-database aczkolwiek, przez robienie tego M$-way można się nabawić miliona magicznych stringów w aplikacji i lepiej stworzyć swój własny atrybut

1 użytkowników online, w tym zalogowanych: 0, gości: 1