0

I've implemented a custom PersistedGrantStore storing my refresh tokens in a xml file, however I now have problems refreshing my tokens.

When I remove following lines, the refresh works

services.AddTransient<IPersistedGrantService, PersistedGrantService>();
services.AddTransient<IPersistedGrantStore, PersistedGrantStore>();

When I put those lines back, I get the following in the logs :

2018-04-24 14:14:51.094 +02:00 [DBG] [IdentityServer4.Hosting.EndpointRouter] Endpoint enabled: Token, successfully created handler: IdentityServer4.Endpoints.TokenEndpoint
2018-04-24 14:14:51.095 +02:00 [INF] [IdentityServer4.Hosting.IdentityServerMiddleware] Invoking IdentityServer endpoint: IdentityServer4.Endpoints.TokenEndpoint for /connect/token
2018-04-24 14:14:51.098 +02:00 [VRB] [IdentityServer4.Endpoints.TokenEndpoint] Processing token request.
2018-04-24 14:14:51.108 +02:00 [DBG] [IdentityServer4.Endpoints.TokenEndpoint] Start token request.
2018-04-24 14:14:51.115 +02:00 [DBG] [IdentityServer4.Validation.ClientSecretValidator] Start client validation
2018-04-24 14:14:51.120 +02:00 [DBG] [IdentityServer4.Validation.BasicAuthenticationSecretParser] Start parsing Basic Authentication secret
2018-04-24 14:14:51.123 +02:00 [DBG] [IdentityServer4.Validation.PostBodySecretParser] Start parsing for secret in post body
2018-04-24 14:14:51.145 +02:00 [DBG] [IdentityServer4.Validation.SecretParser] Parser found secret: PostBodySecretParser
2018-04-24 14:14:51.145 +02:00 [DBG] [IdentityServer4.Validation.SecretParser] Secret id found: UserPortal
2018-04-24 14:14:51.161 +02:00 [DBG] [IdentityServer4.Validation.SecretValidator] Secret validator success: HashedSharedSecretValidator
2018-04-24 14:14:51.161 +02:00 [DBG] [IdentityServer4.Validation.ClientSecretValidator] Client validation success
2018-04-24 14:14:51.167 +02:00 [VRB] [IdentityServer4.Endpoints.TokenEndpoint] Calling into token request validator: IdentityServer4.Validation.TokenRequestValidator
2018-04-24 14:14:51.173 +02:00 [DBG] [IdentityServer4.Validation.TokenRequestValidator] Start token request validation
2018-04-24 14:14:51.181 +02:00 [DBG] [IdentityServer4.Validation.TokenRequestValidator] Start validation of refresh token request
2018-04-24 14:14:51.186 +02:00 [VRB] [IdentityServer4.Validation.TokenValidator] Start refresh token validation
2018-04-24 14:14:51.228 +02:00 [DBG] [Nextel.VisitorManager.IdentityServer.Extensions.PersistedGrantStore] r9bsqJEQddM1Hhp6PzZwk/Zr3Yyb72PntfPZup+ik5Y= found in database: True
2018-04-24 14:14:51.992 +02:00 [ERR] [IdentityServer4.Validation.TokenValidator] Refresh token has expired. Removing from store.
2018-04-24 14:14:51.996 +02:00 [DBG] [Nextel.VisitorManager.IdentityServer.Extensions.PersistedGrantStore] removing r9bsqJEQddM1Hhp6PzZwk/Zr3Yyb72PntfPZup+ik5Y= persisted grant from database
2018-04-24 14:14:52.002 +02:00 [ERR] [IdentityServer4.Validation.TokenRequestValidator] Refresh token validation failed. aborting.
2018-04-24 14:14:52.095 +02:00 [ERR] [IdentityServer4.Validation.TokenRequestValidator] {
  "ClientId": "UserPortal",
  "ClientName": "User Portal Client",
  "GrantType": "refresh_token",
  "Raw": {
    "client_id": "UserPortal",
    "client_secret": "***REDACTED***",
    "grant_type": "refresh_token",
    "refresh_token": "3314d145ec7dec80b8137b288c99a66b22798451dd77a8878466139da29a3c13",
    "scope": "offline_access"
  }
}
2018-04-24 14:14:52.098 +02:00 [VRB] [IdentityServer4.Hosting.IdentityServerMiddleware] Invoking result: IdentityServer4.Endpoints.Results.TokenErrorResult

Which seems to show it does find the refresh token in the store, but then deletes it and returns an invalid_grant

This is my PersistedGrantStore:

    public class PersistedGrantStore : IPersistedGrantStore
    {
        #region Declarations

        private readonly ILogger logger;
        private readonly IPersistedGrantService persistedGrantService;

        #endregion

        #region Constructor

        public PersistedGrantStore(IPersistedGrantService persistedGrantService, ILogger<PersistedGrantStore> logger)
        {
            this.persistedGrantService = persistedGrantService;
            this.logger = logger;
        }

        #endregion

        #region Methods

        public Task StoreAsync(PersistedGrant token)
        {
            var existing = persistedGrantService.GetByKey(token.Key);

            if (existing == null)
            {
                logger.LogDebug($"{token.Key} not found in database");
                persistedGrantService.Add(token);
            }
            else
            {
                logger.LogDebug($"{token.Key} found in database");
                persistedGrantService.Update(token);
            }

            return Task.FromResult(0);
        }

        public Task<PersistedGrant> GetAsync(string key)
        {
            var persistedGrant = persistedGrantService.GetByKey(key);

            logger.LogDebug($"{key} found in database: {persistedGrant != null}");

            return Task.FromResult(persistedGrant);
        }

        public Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId)
        {
            var persistedGrants = persistedGrantService.GetBySubject(subjectId);

            logger.LogDebug($"{persistedGrants.Count} persisted grants found for {subjectId}");

            return Task.FromResult(persistedGrants.AsEnumerable());
        }

        public Task RemoveAsync(string key)
        {
            var persistedGrant = persistedGrantService.GetByKey(key);
            if (persistedGrant != null)
            {
                logger.LogDebug($"removing {key} persisted grant from database");

                persistedGrantService.Remove(persistedGrant);
            }
            else
                logger.LogDebug($"no {key} persisted grant found in database");

            return Task.FromResult(0);
        }

        public Task RemoveAllAsync(string subjectId, string clientId)
        {
            var persistedGrants = persistedGrantService.GetBySubjectClient(subjectId, clientId);

            logger.LogDebug($"removing {persistedGrants.Count} persisted grants from database for subject {subjectId}, clientId {clientId}");

            persistedGrantService.Remove(persistedGrants);

            return Task.FromResult(0);
        }

        public Task RemoveAllAsync(string subjectId, string clientId, string type)
        {
            var persistedGrants = persistedGrantService.GetBySubjectClientType(subjectId, clientId, type);

            logger.LogDebug($"removing {persistedGrants.Count} persisted grants from database for subject {subjectId}, clientId {clientId}, grantType {type}");

            persistedGrantService.Remove(persistedGrants);

            return Task.FromResult(0);
        }

        #endregion
    }

And this is my PersistedGrantService:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Serialization;
using IdentityServer4.Models;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;

namespace Nextel.VisitorManager.IdentityServer.Extensions
{
    public class PersistedGrantService : IPersistedGrantService
    {
        #region Declarations

        private readonly ILogger logger;
        private List<PersistedGrant> persistedGrants;
        private readonly string xmlFile;

        #endregion

        #region Constructor

        public PersistedGrantService(IHostingEnvironment env, ILogger<PersistedGrantStore> logger)
        {
            this.logger = logger;

            xmlFile = env.ContentRootPath + @"\PersistedGrants.xml";
        }

        #endregion

        #region Methods

        public PersistedGrant GetByKey(string key)
        {
            if (persistedGrants != null || ReadXml())
                return persistedGrants.FirstOrDefault(x => x.Key == key);

            return null;
        }

        public List<PersistedGrant> GetBySubject(string subjectId)
        {
            if (persistedGrants != null || ReadXml())
                return persistedGrants.Where(x => x.SubjectId == subjectId).ToList();

            return null;
        }

        public List<PersistedGrant> GetBySubjectClient(string subjectId, string clientId)
        {
            if (persistedGrants != null || ReadXml())
                return persistedGrants.Where(x => x.SubjectId == subjectId && x.ClientId == clientId).ToList();

            return null;
        }

        public List<PersistedGrant> GetBySubjectClientType(string subjectId, string clientId, string type)
        {
            if (persistedGrants != null || ReadXml())
                return persistedGrants.Where(x => x.SubjectId == subjectId && x.ClientId == clientId && x.Type == type).ToList();

            return null;
        }

        public void Add(PersistedGrant item)
        {
            if (persistedGrants != null || ReadXml())
            {
                persistedGrants.Add(item);
                SaveXml();
            }
        }

        public void Update(PersistedGrant item)
        {
            if (persistedGrants != null || ReadXml())
            {
                var index = persistedGrants.FindIndex(x => x.Key == item.Key);
                persistedGrants[index] = item;
                SaveXml();
            }
        }

        public void Remove(PersistedGrant item)
        {
            if (persistedGrants != null || ReadXml())
            {
                persistedGrants.Remove(item);
                SaveXml();
            }
        }

        public void Remove(List<PersistedGrant> items)
        {
            if (persistedGrants != null || ReadXml())
            {
                foreach (var item in items)
                {
                    persistedGrants.Remove(item);
                }

                SaveXml();
            }
        }

        #region Privates

        private bool ReadXml()
        {
            try
            {
                if (File.Exists(xmlFile))
                {
                    var deserializer = new XmlSerializer(typeof(List<PersistedGrant>));
                    using (TextReader reader = new StreamReader(xmlFile))
                    {
                        persistedGrants = (List<PersistedGrant>)deserializer.Deserialize(reader);
                    }
                }
                else
                    persistedGrants = new List<PersistedGrant>();

                return true;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Error in reading XML");
                return false;
            }
        }

        private bool SaveXml()
        {
            try
            {
                if (persistedGrants != null)
                {
                    var serializer = new XmlSerializer(typeof(List<PersistedGrant>));
                    using (TextWriter writer = new StreamWriter(xmlFile))
                    {
                        serializer.Serialize(writer, persistedGrants);
                    }
                }

                return true;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Error in saving XML");
                return false;
            }
        }

        #endregion

        #endregion
    }
}

With this interface:

    public interface IPersistedGrantService
    {
        PersistedGrant GetByKey(string key);
        List<PersistedGrant> GetBySubject(string subjectId);
        List<PersistedGrant> GetBySubjectClient(string subjectId, string clientId);
        List<PersistedGrant> GetBySubjectClientType(string subjectId, string clientId, string type);
        void Add(PersistedGrant item);
        void Update(PersistedGrant item);
        void Remove(PersistedGrant item);
        void Remove(List<PersistedGrant> items);


    }
Tom Janssens
  • 1
  • 1
  • 1
  • The first message is: "Refresh token has expired. Removing from store." –  Apr 25 '18 at 21:08

1 Answers1

3

I had almost similar code for refreshing the token and was spending hours together to resolve the same issue. Found out that the Client configuration should have "RefreshTokenExpiration" attribute set as TokenExpiration.Absolute and "RefreshTokenUsage" attribute is set to "OneTimeOnly". This should make it work !

Pravin
  • 839
  • 7
  • 12