15
votes

I'm using Identity Server 3 to authenticate and generate Access/Refresh tokens for my angular Client.

I'm currently setting the Refresh Token to expire in 48 hours for my Angular Client.

Some users who use my Angular application will need to be Signed On for 100 days straight without having to re-enter their credentials, is it possible to set the expiration of my Refresh Token for a specific user only instead of the entire client?

I have 100 users in my database, I want just one specific user to not need to re-authenticate in 100 days while the rest should authenticate every 48 hours.

Something along the lines of:

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

Is this possible to achieve? or am I restricted to only setting the Refresh Token Expiration for the Entire Client?

Thank You

3

3 Answers

5
votes

I've never worked with IdentityServer3 and I didn't test the code below, but I think the concept may work.

When I take a look at the code of IdentityServer3 then I can see that in DefaultRefreshTokenService.CreateRefreshTokenAsync the lifetime is set:

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;
}

You wouldn't want to change the core code, but you should be able to override the IRefreshTokenService with your own implementation.

When I take the code from CustomUserService sample as example:

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);

Where MyDefaultRefreshTokenService is a copy of the DefaultRefreshTokenService.

In order to make it compile add a NuGet package of IdentityModel (v1.13.1) and add the following class:

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;
        }
    }
}

Now there are some compilation errors concerning the events. You can remove the events in order to test the code. If it works you can always choose to add them.

And now for the implementation of the RefreshTokenLifetime per user. In your version of the RefreshTokenService you can remove the client code and use your own logic to determine the lifetime per user.

The subject is available, though I don't know if it already contains enough information. But if it does then you can access the userManager to read the lifetime from the store. Or use an alternative to pass the lifetime information (perhaps you can use a claim containing the lifetime value).

Again, I didn't test this, but I think the concept should work.

3
votes

Considerations

Consider sliding sessions for example. With sliding sessions, you would send a new short-lived token with every authenticated action made by the user. As long as the user is active he will stay authenticated (e.g. it requires user interaction before expiration interval, although it requires token management implementations). If the user sends an expired token, it means he has been inactive for a while.

Let's see how JWT works:

JWT snapshot

The JWT is mainly suitable for the following cases:

  • In case of building API services that need to support server-to-server or client-to-server (like a mobile app or single page app (SPA)) communication, using JWTs as your API tokens is a very smart idea (clients will be making requests frequently, with limited scope, and usually authentication data can be persisted in a stateless way without too much dependence on user data).
  • If you’re building any type of service where you need three or more parties involved in a request, JWTs can also be useful.
  • if you’re using user federation (things like single sign-on and OpenID Connect), JWTs become important because you need a way to validate a user’s identity via a third party.

more clarification at stop using jwts as session tokens

So Stop using JWT for sessions, it’s a bad idea to use JWTs as session tokens for most of cases.

Possible Solution

For Refreshing JWT, the JWT refresh tokens and .NET Core may be useful to implement your own code And descriptions inside JWT (JSON Web Token) automatic prolongation of expiration guides you to design a working scenario. You need to inspect desired user before refreshing operation.

I found another implementation at Handle Refresh Token Using ASP.NET Core 2.0 And JSON Web Token for you, maybe useful.

0
votes

I'm not familiar with Microsoft's Identity Server (the "Identity Service" I refer to in the code below is a custom implementation), but you could consider writing an authentication handler to intercept the token in HTTP headers, examine a token prefix, then decide whether to process normally or allow an extended lifetime.

In my case, I intercept the token prior to JWT processing it. (I had to do this to get around a SharePoint workflow limitation. Oh, SharePoint.) Here's the AuthenticationHandler class:

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."));
        }
    }
}

Then in the Startup class of your service, add code to decide which authentication handler to use. The key feature here is the 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)
        };
    });

Add an extension method to the AuthenticationBuilder class:

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

And authentication options if you need them.

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();
        }
    }
}

AuthenticationIdentities is just a class I define to associate a token with a username, roles, and claims (the token for the SharePoint workflow engine). It's populated from appsettings.json. Your options class most likely would contain a list of users who are authorized for an extended lifetime.

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>();
        }
    }
}