.NET Web API: установите другое время обновления токена обновления для разных пользователей

Я использую Identity Server 3 для аутентификации и генерации токенов доступа/обновления для моего углового клиента.

В настоящее время я устанавливаю токен обновления для истечения через 48 часов для моего Углового клиента.

Некоторые пользователи, которые используют мое приложение "Угловое", должны быть подписаны на 100 дней подряд без повторного ввода своих учетных данных, можно ли установить срок действия моего токена обновления только для конкретного пользователя, а не для всего клиента?

У меня есть 100 пользователей в моей базе данных, я хочу, чтобы только один конкретный пользователь не нуждался в повторной аутентификации через 100 дней, в то время как остальные должны проходить проверку подлинности каждые 48 часов.

Что-то вроде:

if (user == "Super Man") {
    AbsoluteRefreshTokenLifetime = TimeSpan.FromDays(100.0).Seconds,
}

Можно ли это достичь? или я ограничился только установкой срока действия токена обновления для всего клиента?

Благодарю вас

Ответ 1

Я никогда не работал с IdentityServer3, и я не тестировал код ниже, но я думаю, что эта концепция может работать.

Когда я смотрю на код IdentityServer3, я вижу, что в DefaultRefreshTokenService.CreateRefreshTokenAsync установлено время жизни:

int lifetime;
if (client.RefreshTokenExpiration == TokenExpiration.Absolute)
{
    Logger.Debug("Setting an absolute lifetime: " + client.AbsoluteRefreshTokenLifetime);
    lifetime = client.AbsoluteRefreshTokenLifetime;
}
else
{
    Logger.Debug("Setting a sliding lifetime: " + client.SlidingRefreshTokenLifetime);
    lifetime = client.SlidingRefreshTokenLifetime;
}

Вы не захотите изменить основной код, но вы должны переопределить IRefreshTokenService своей собственной реализацией.

Когда я беру код из примера CustomUserService в качестве примера:

internal class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.Map("/core", coreApp =>
        {
            var factory = new IdentityServerServiceFactory()
                .UseInMemoryClients(Clients.Get())
                .UseInMemoryScopes(Scopes.Get());

            var refreshTokenService = new MyDefaultRefreshTokenService();

            // note: for the sample this registration is a singletone (not what you want in production probably)
            factory.RefreshTokenService = new Registration<IrefreshTokenService>(resolver => refreshTokenService);

Где MyDefaultRefreshTokenService является копией DefaultRefreshTokenService.

Чтобы сделать компиляцию, добавьте пакет NuGet IdentityModel (v1.13.1) и добавьте следующий класс:

using System;

namespace IdentityServer3.Core.Extensions
{
    internal static class DateTimeOffsetHelper
    {
        internal static Func<DateTimeOffset> UtcNowFunc = () => DateTimeOffset.UtcNow;

        internal static DateTimeOffset UtcNow
        {
            get
            {
                return UtcNowFunc();
            }
        }

        internal static int GetLifetimeInSeconds(this DateTimeOffset creationTime)
        {
            return (int)(UtcNow - creationTime).TotalSeconds;
        }
    }
}

Теперь есть некоторые ошибки компиляции в отношении событий. Вы можете удалить события, чтобы проверить код. Если он работает, вы всегда можете добавить их.

И теперь для реализации RefreshTokenLifetime для каждого пользователя. В вашей версии RefreshTokenService вы можете удалить клиентский код и использовать свою собственную логику для определения времени жизни для каждого пользователя.

Тема доступна, хотя я не знаю, содержит ли она уже достаточную информацию. Но если это произойдет, вы можете получить доступ к userManager, чтобы прочитать время жизни из магазина. Или используйте альтернативу для передачи информации о жизни (возможно, вы можете использовать заявку, содержащую значение времени жизни).

Опять же, я не тестировал это, но я думаю, что концепция должна работать.

Ответ 2

Соображения

Рассмотрим, например, скользящие сессии. С раздвижными сеансами вы отправляете новый кратковременный токен с каждым подтвержденным действием, сделанным пользователем. Пока пользователь активен, он остается аутентифицированным (например, он требует взаимодействия с пользователем до истечения срока действия, хотя для него требуются реализации управления токеном). Если пользователь отправляет истекший токен, это означает, что он некоторое время неактивен.

Посмотрим, как работает JWT:

JWT snapshot

JWT в основном подходит для следующих случаев:

  • В случае создания API-сервисов, которые должны поддерживать связь между сервером или клиентом-сервером (например, мобильное приложение или одностраничное приложение (SPA)), использование JWT в качестве токенов API - очень умная идея (клиенты будут часто запрашивать запросы, с ограниченными возможностями, и обычно данные аутентификации могут сохраняться без учета состояния без чрезмерной зависимости от пользовательских данных).
  • Если вы создаете какой-либо сервис, в котором вам нужны три или более стороны, участвующие в запросе, JWT также могут быть полезны.
  • если вы используете пользовательскую федерацию (такие как единый вход и OpenID Connect), JWT становятся важными, потому что вам нужен способ проверки подлинности пользователя через стороннюю компанию.

больше разъяснений при остановке использования jwts в качестве токенов сеанса

Поэтому прекратите использование JWT для сеансов, его плохая идея использовать JWT в качестве токенов сеанса для большинства случаев.

Возможное решение

Для обновления JWT токены обновления JWT и.NET Core могут быть полезны для реализации вашего собственного кода. И описания внутри JWT (JSON Web Token) автоматически продлевают срок действия, чтобы вы могли разработать рабочий сценарий. Перед обновлением необходимо проверить желаемого пользователя.

Я нашел другую реализацию в Handle Refresh Token, используя ASP.NET Core 2.0 и JSON Web Token для вас, возможно, полезный.

Ответ 3

Я не знаком с Microsoft Identity Server ("Служба идентификации", на которую я ссылаюсь в приведенном ниже коде, представляет собой пользовательскую реализацию), но вы можете подумать о том, чтобы написать обработчик проверки подлинности, чтобы перехватить токен в заголовках HTTP, изучить префикс-токен, затем решить, следует ли нормально обрабатывать или разрешить продление срока службы.

В моем случае я перехватываю токен перед обработкой JWT. (Я должен был сделать это, чтобы обойти ограничение рабочего процесса SharePoint. О, SharePoint.) Здесь класс AuthenticationHandler:

using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;


namespace CompanyName.Core2.Application.Middleware
{
    [UsedImplicitly]
    public class AuthenticationHandler : AuthenticationHandler<AuthenticationOptions>
    {
        public const string AuthenticationScheme = "CompanyName Token";
        [UsedImplicitly] public const string HttpHeaderName = "Authorization";
        [UsedImplicitly] public const string TokenPrefix = "CompanyName ";


        public AuthenticationHandler(IOptionsMonitor<AuthenticationOptions> Options, ILoggerFactory Logger, UrlEncoder Encoder, ISystemClock Clock)
            : base(Options, Logger, Encoder, Clock)
        {
        }


        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            if (!Request.Headers.TryGetValue(HttpHeaderName, out StringValues authorizationValues))
            {
                // Indicate failure.
                return await Task.FromResult(AuthenticateResult.Fail($"{HttpHeaderName} header not found."));
            }
            string token = authorizationValues.ToString();
            foreach (AuthenticationIdentity authenticationIdentity in Options.Identities)
            {
                if (token == $"{TokenPrefix}{authenticationIdentity.Token}")
                {
                    // Authorization token is valid.
                    // Create claims identity, add roles, and add claims.
                    ClaimsIdentity claimsIdentity = new ClaimsIdentity(AuthenticationScheme);
                    claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, authenticationIdentity.Username));
                    foreach (string role in authenticationIdentity.Roles)
                    {
                        claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role));
                    }
                    foreach (string claimType in authenticationIdentity.Claims.Keys)
                    {
                        string claimValue = authenticationIdentity.Claims[claimType];
                        claimsIdentity.AddClaim(new Claim(claimType, claimValue));
                    }
                    // Create authentication ticket and indicate success.
                    AuthenticationTicket authenticationTicket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), Scheme.Name);
                    return await Task.FromResult(AuthenticateResult.Success(authenticationTicket));
                }
            }
            // Indicate failure.
            return await Task.FromResult(AuthenticateResult.Fail($"Invalid {HttpHeaderName} header."));
        }
    }
}

Затем в классе Startup вашего сервиса добавьте код, чтобы решить, какой обработчик для проверки подлинности использовать. Ключевой особенностью здесь является ForwardDefaultSelector:

public void ConfigureServices(IServiceCollection Services)
{
    // Require authentication token.
    // Enable CompanyName token for SharePoint workflow client, which cannot pass HTTP headers > 255 characters (JWT tokens are > 255 characters).
    // Enable JWT token for all other clients.  The JWT token specifies the security algorithm used when it was signed (by Identity service).
    Services.AddAuthentication(AuthenticationHandler.AuthenticationScheme).AddCompanyNameAuthentication(Options =>
    {
        Options.Identities = Program.AppSettings.AuthenticationIdentities;
        Options.ForwardDefaultSelector = HttpContext =>
        {
            // Forward to JWT authentication if CompanyName token is not present.
            string token = string.Empty;
            if (HttpContext.Request.Headers.TryGetValue(AuthenticationHandler.HttpHeaderName, out StringValues authorizationValues))
            {
                token = authorizationValues.ToString();
            }
            return token.StartsWith(AuthenticationHandler.TokenPrefix)
                ? AuthenticationHandler.AuthenticationScheme
                : JwtBearerDefaults.AuthenticationScheme;
        };
    })
    .AddJwtBearer(Options =>
    {
        Options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Program.AppSettings.ServiceOptions.TokenSecret)),
            ValidateIssuer = false,
            ValidateAudience = false,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromMinutes(_clockSkewMinutes)
        };
    });

Добавьте метод расширения в класс AuthenticationBuilder:

public static AuthenticationBuilder AddCompanyNameAuthentication(this AuthenticationBuilder AuthenticationBuilder, Action<AuthenticationOptions> ConfigureOptions = null)
{
    return AuthenticationBuilder.AddScheme<AuthenticationOptions, AuthenticationHandler>(AuthenticationHandler.AuthenticationScheme, ConfigureOptions);
}

И параметры аутентификации, если они вам понадобятся.

using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;


namespace CompanyName.Core2.Application.Middleware
{
    public class AuthenticationOptions : AuthenticationSchemeOptions
    {
        [UsedImplicitly]
        public AuthenticationIdentities Identities { get; [UsedImplicitly] set; }


        public AuthenticationOptions()
        {
            Identities = new AuthenticationIdentities();
        }
    }
}

АутентификацияIdentities - это только класс, который я определяю, чтобы связать токен с именем пользователя, ролями и претензиями (токен для механизма рабочего процесса SharePoint). Он заселен из appsettings.json. Ваш класс параметров, скорее всего, будет содержать список пользователей, которым разрешено продлить срок службы.

using System.Collections.Generic;
using JetBrains.Annotations;


namespace CompanyName.Core2.Application.Middleware
{
    public class AuthenticationIdentity
    {
        public string Token { get; [UsedImplicitly] set; }
        public string Username { get; [UsedImplicitly] set; }
        [UsedImplicitly] public List<string> Roles { get; [UsedImplicitly] set; }
        [UsedImplicitly] public Dictionary<string, string> Claims { get; [UsedImplicitly] set; }


        public AuthenticationIdentity()
        {
            Roles = new List<string>();
            Claims = new Dictionary<string, string>();
        }
    }
}