Search code examples
c#asp.net-coreredisidentityserver4

How to create a persistent ticket in Redis


I have an Identity server 4 application which i am trying to get to remember (persist) the user login.

I think i have tracked the issue down to something in Redis.

When the tickets are stored in redis they are stored with a time out on them.

127.0.0.1:6379> ttl Ids-v2-Local-Key-74c112d5-e0f4-48c4-9d0f-cd8e62f12dfd
(integer) 3014

As long as the application is open redis will refresh the time out.

1542808250.760394 [0 lua] "EXPIRE" "Ids-v2-Local-Key-74c112d5-e0f4-48c4-9d0f-cd8e62f12dfd" "3600"

This is fine as long as the user is active on the application they key continues to be refreshed. However if the user goes home and comes back the next day their application is no longer logged in.

I was able to fix this by manually logging in to redis and setting the key to Persist

127.0.0.1:6379> Persist XenaIdentityserver-v2-Local-Key-74c112d5-e0f4-48c4-9d0f-cd8e62f12dfd
(integer) 1
127.0.0.1:6379> ttl XenaIdentityserver-v2-Local-Key-74c112d5-e0f4-48c4-9d0f-cd8e62f12dfd
(integer) -1

I think the issue is how the keys are created in redis.

RedisCacheTicketStore

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Redis;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;

namespace Xena.IdentityServer.Services
{
    public class RedisCacheTicketStore : ITicketStore
    {
        private readonly ILogger _logger;
        private string KeyPrefix = "AuthSessionStore-";
        private IDistributedCache _cache;

        public RedisCacheTicketStore(RedisCacheOptions options, ILogger logger, IConfiguration config)
        {
            KeyPrefix = config["Redis:ApplicationName"] + "-";

            _logger = logger;
            _cache = new RedisCache(options);
        }

        public async Task<string> StoreAsync(AuthenticationTicket ticket)
        {
            var sw = new Stopwatch();
            sw.Start();

            var guid = Guid.NewGuid();
            var key = KeyPrefix + guid.ToString();
            await RenewAsync(key, ticket);

            _logger.LogDebug(LoggingEvents.RedisCacheTicketStore, "Redis Method StoreAsync Elapsed {sw.ElapsedMilliseconds}", sw.ElapsedMilliseconds);

            return key;
        }

        public Task RenewAsync(string key, AuthenticationTicket ticket)
        {
            var sw = new Stopwatch();
            sw.Start();

            var options = new DistributedCacheEntryOptions();
            var expiresUtc = ticket.Properties.ExpiresUtc;
            if (expiresUtc.HasValue)
            {
                options.SetAbsoluteExpiration(expiresUtc.Value);
            }

            options.SetSlidingExpiration(TimeSpan.FromMinutes(60));

            byte[] val = SerializeToBytes(ticket, _logger);
            _cache.Set(key, val, options);
            sw.Stop();
            _logger.LogDebug(LoggingEvents.RedisCacheTicketStore, "Redis Method RenewAsync Elapsed {sw.ElapsedMilliseconds}", sw.ElapsedMilliseconds);
            return Task.FromResult(0);
        }

        public Task<AuthenticationTicket> RetrieveAsync(string key)
        {
            var sw = new Stopwatch();
            sw.Start();

            AuthenticationTicket ticket;
            byte[] bytes = null;
            bytes = _cache.Get(key);
            ticket = DeserializeFromBytes(bytes, _logger);

            sw.Stop();
            _logger.LogDebug(LoggingEvents.RedisCacheTicketStore, "Redis Method RetrieveAsync Elapsed {sw.ElapsedMilliseconds}", sw.ElapsedMilliseconds);

            return Task.FromResult(ticket);
        }

        public Task RemoveAsync(string key)
        {
            var sw = new Stopwatch();
            sw.Start();

            _cache.Remove(key);

            sw.Stop();
            _logger.LogDebug(LoggingEvents.RedisCacheTicketStore, "Redis Method RemoveAsync Elapsed {sw.ElapsedMilliseconds}", sw.ElapsedMilliseconds);


            return Task.FromResult(0);
        }

        private static byte[] SerializeToBytes(AuthenticationTicket source, ILogger logger)
        {
            var sw = new Stopwatch();
            sw.Start();

            var ticket = TicketSerializer.Default.Serialize(source);

            sw.Stop();
            logger.LogDebug(LoggingEvents.RedisCacheTicketStore, "Redis Method SerializeToBytes Elapsed {sw.ElapsedMilliseconds}", sw.ElapsedMilliseconds);

            return ticket;
        }

        private static AuthenticationTicket DeserializeFromBytes(byte[] source, ILogger logger)
        {
            var sw = new Stopwatch();
            sw.Start();

            var hold = source == null ? null : TicketSerializer.Default.Deserialize(source);

            sw.Stop();
            logger.LogDebug(LoggingEvents.RedisCacheTicketStore, "Redis Method DeserializeFromBytes Elapsed {sw.ElapsedMilliseconds}", sw.ElapsedMilliseconds);

            return hold;
        }   

    }
}

I can go though the code and see that the AuthenticationTicket is set to isPersistant when going though the StoreAsync method. However it doesnt create a persistent ticket it still has a time out on it.

enter image description here

How do i tell _cache.Set(key, val, options); to set a persistent ticket and not one with a timeout?


Solution

  • After tip from Kirk Larkin in a comment. It occurred to me that all i really needed to do was set the timeout longer then it would continue to slide it as long as the user was active. If i had set it to Persist then it would have been in redis forever and thats really not what we want. We want it in there for a given period of time as long as the user comes back from time to time they will continue to appear to be logged in.

    public Task RenewAsync(string key, AuthenticationTicket ticket)
            {
                var options = new DistributedCacheEntryOptions();
                var expiresUtc = ticket.Properties.ExpiresUtc;
                if (expiresUtc.HasValue)
                    options.SetAbsoluteExpiration(expiresUtc.Value);
    
                if (ticket.Properties.IsPersistent && !expiresUtc.HasValue)
                    options.SetSlidingExpiration(_rememberMeTimeoutInDays);
                else if (ticket.Properties.IsPersistent && expiresUtc.HasValue)
                    options.SetSlidingExpiration(TimeSpan.FromTicks(expiresUtc.Value.Ticks));
                else
                    options.SetSlidingExpiration(_defaultTimeoutInHours);
    
                byte[] val = SerializeToBytes(ticket, _logger);
                _cache.Set(key, val, options);
                return Task.FromResult(0);
            }