Запретить пользователям проводить несколько сеансов с токенами JWT

Я создаю приложение, которое использует аутентификацию носителя JWT в ASP.NET Core. Мне нужно запретить пользователям открывать несколько сеансов одновременно. Мне интересно, есть ли способ с помощью промежуточного программного обеспечения Microsoft.AspNetCore.Authentication.JwtBearer перечислить все токены пользователя, а затем проверить, есть ли другие токены, выпущенные для этого пользователя, чтобы аннулировать входящий запрос аутентификации.

Если утверждения могут быть проверены на сервере, я предполагаю, что для этого на сервере есть запись этих утверждений и пользователя, которому они принадлежат. Правильно?

Любые идеи, как я могу этого добиться?


person D.B    schedule 15.07.2017    source источник
comment
Используйте ссылочные токены, и когда пользователь переходит в другой журнал, отзывайте все старые токены.   -  person Mardoxx    schedule 15.07.2017
comment
ссылочные токены? Не могли бы вы уточнить свой ответ? Спасибо.   -  person D.B    schedule 16.07.2017
comment
Или один пример был бы отличным. Спасибо.   -  person D.B    schedule 16.07.2017
comment
@Mardoxx прав. Для получения дополнительной информации см. youtu.be/BdKmZ7mPNns?t=12m54s Другими словами: функцию, которую вы Если вы не добавите поддержку отзыва токена, с JWT не получится реализовать. Если вы это сделаете, вы потеряете основное преимущество JWT (без сохранения состояния), поэтому следует использовать ссылочные токены.   -  person Spomky-Labs    schedule 17.07.2017
comment
Или реализовать отзыв с сохранением даты последнего токена выдачи для каждого пользователя и не принимать предыдущие токены. Вы по-прежнему теряете JWT без состояния, но реализация легковесна   -  person pedrofb    schedule 17.07.2017
comment
Спасибо, парни. Хорошо, я достиг своей цели, сохранив в базе данных метку времени при входе пользователя в систему, добавив эту метку времени в полезную нагрузку токена, а затем добавив дополнительный уровень безопасности для проверки JWT по сравнению с базой данных, возвращая 401, если метка времени не совпадает.   -  person D.B    schedule 19.07.2017
comment
В зависимости от того, какие строки вы используете, они могут уже быть включены в токен в качестве утверждения iat.   -  person Mardoxx    schedule 22.07.2017


Ответы (1)


Я достиг своей цели, сохранив в базе данных метку времени при входе пользователя в систему, добавив эту метку времени в полезную нагрузку токена, а затем добавив дополнительный уровень безопасности для проверки JWT против базы данных, вернув 401, если метка времени не совпадение. Это код, реализованный в .net Core 2.0, если кому-то это нужно.

Контроллер:

    [HttpPost]
    [Route("authenticate")]
    public async Task<IActionResult> AuthenticateAsync([FromBody] UserModel user)
    {
        try
        {
            .......

            if (userSecurityKey != null)
            {
                var tokenHandler = new JwtSecurityTokenHandler();
                var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
                var tokenDescriptor = new SecurityTokenDescriptor
                {
                    Subject = new ClaimsIdentity(new Claim[]
                    {
                       // This claim allows us to store information and use it without accessing the db
                        new Claim("userSecurityKey", userDeserialized.SecurityKey.ToString()),
                        new Claim("timeStamp",timeStamp),
                        new Claim("verificationKey",userDeserialized.VerificationKey.ToString()),
                        new Claim("userName",userDeserialized.UserName)

                    }),
                    Expires = DateTime.UtcNow.AddDays(7),
                    SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key),
                        SecurityAlgorithms.HmacSha256Signature)
                };
                var token = tokenHandler.CreateToken(tokenDescriptor);
                var tokenString = tokenHandler.WriteToken(token);

               // Updates timestamp for the user if there is one
                VerificationPortalTimeStamps userTimeStamp = await _context.VerificationPortalTimeStamps.AsNoTracking().FirstOrDefaultAsync(e => e.UserName == userDeserialized.UserName);

                if (userTimeStamp != null)
                {
                    userTimeStamp.TimeStamp = timeStamp;
                    _context.Entry(userTimeStamp).State = EntityState.Modified;
                   await _context.SaveChangesAsync();
                }
                else
                {
                    _context.VerificationPortalTimeStamps.Add(new VerificationPortalTimeStamps { TimeStamp = timeStamp, UserName = userDeserialized.UserName });
                    await _context.SaveChangesAsync();
                }


                // return basic user info (without password) and token to store client side                   
                return Json(new
                {
                    userName = userDeserialized.UserName,
                    userSecurityKey = userDeserialized.SecurityKey,
                    token = tokenString
                });
            }

            return Unauthorized();

        }
        catch (Exception)
        {
            return Unauthorized();
        }
    }

Затем, чтобы настроить аутентификацию носителя JWT с .Net Core 2.0

Startup.cs:

 public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IServiceProvider provider)
    {
        .................     

        app.UseAuthentication();

        app.UseMvc();

        ...........

    }

Чтобы настроить аутентификацию на носителе JWT:

public IServiceProvider ConfigureServices(IServiceCollection services)
    {

        ............


        var key = Configuration["AppSettings:Secret"];

        byte[] keyAsBytes = Encoding.ASCII.GetBytes(key);

        services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
            .AddJwtBearer(o =>
            {
                o.RequireHttpsMetadata = false;
                o.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(keyAsBytes),
                    ValidateIssuer = false,
                    ValidateAudience = false,
                    ValidateLifetime = true

                };

                o.Events = new JwtBearerEvents
                {
                    OnAuthenticationFailed = context =>
                    {
                        if (Configuration["AppSettings:IsGodMode"] != "true")
                            context.Response.StatusCode = 401;


                        return Task.FromResult<object>(0);
                    }
                };
                o.SecurityTokenValidators.Clear();
                o.SecurityTokenValidators.Add(new MyTokenHandler());
            });

        services.AddMvc()                
            .AddJsonOptions(opt =>
            {
                opt.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
            });


        var provider = services.BuildServiceProvider();


        return provider;
    }

Затем мы реализуем настраиваемую проверку в контроллере следующим образом:

    [Authorize]
    [HttpGet]
    [Route("getcandidate")]
    public async Task<IActionResult> GetCandidateAsync()
    {

        try
        {
            .....

            //Get user from the claim
            string userName = User.FindFirst("UserName").Value;

            //Get timestamp from the db for the user
            var currentUserTimeStamp = _context.VerificationPortalTimeStamps.AsNoTracking().FirstOrDefault(e => e.UserName == userName).TimeStamp;

           // Compare timestamp from the claim against timestamp from the db
            if (User.FindFirst("timeStamp").Value != currentUserTimeStamp)
            {
                return NotFound();
            }

            ...........

        }
        catch (Exception)
        {
            return NotFound();
        }
    }
person D.B    schedule 15.09.2017