Upgrade IdentityServer4 from 1.0.0-beta5 to RC1

Just upgraded twee.cloud to use the IdentityServer4 RC1 version. There was a few changes that wasn't totally clear. So I will write them down here and an kinda step by step guide, What I needed to do to get IdentityServer4 working. It should also be noted that I only use a small part of IdentityServer4 so there could be more "breaking changes" that I don't cover here.

First

of all namespace changes if you still use the sample code / in-memory. Changed from IdentityServer4.Services.InMemory to IdentityServer4.Quickstart.

Second

problem I got into in this code:

new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", Constants.ClaimValueTypes.Json)  

It will complain about Constants.ClaimValueTypes.Json they are now located here IdentityServerConstants.ClaimValueTypes.Json

Third

problem I got into was that I have implemented an custom ITokenHandleStore and IRefreshTokenStore. They are now merged into a single interface that's called IPersistedGrantStore that almost have the same methods the biggest change is a new RemoveAllAsync(string subjectId, string clientId, string type) so it take in a type also. So it can delete by type.

I have made an implementation for Redis here is the full implementation.

using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Threading.Tasks;  
using IdentityServer4.Models;  
using IdentityServer4.Services;  
using IdentityServer4.Stores;  
using Newtonsoft.Json;  
using StackExchange.Redis;  
using twee.cloud.auth.Infrastructure;

namespace twee.cloud.auth.Services  
{
    public class RedisTokenHandleStore : IPersistedGrantStore
    {
        private readonly IDatabase _redis;
        private string _baseKey = "IdentityServer:Token";
        private string _subjectKey = ":Subject:";
        private string _ClientKey = ":Client:";

        public RedisTokenHandleStore(IDatabase redis)
        {
            _redis = redis;
        }


        public async Task StoreAsync(PersistedGrant grant)
        {
            await _redis.SetAddAsync(_baseKey + _subjectKey + grant.SubjectId, _baseKey + grant.Key);
            await _redis.SetAddAsync(_baseKey + _subjectKey + grant.SubjectId + grant.Type, _baseKey + grant.Key);
            await _redis.SetAddAsync(_baseKey + _subjectKey + _ClientKey + grant.SubjectId + grant.ClientId, _baseKey + grant.Key);
            await _redis.StringSetAsync(_baseKey + grant.Key, JsonConvert.SerializeObject(grant, JsonNetSettings.GetJsonSerializerSettings()), expiry: grant.Expiration - grant.CreationTime );

        }

        async Task<PersistedGrant> IPersistedGrantStore.GetAsync(string key)
        {
            var result = await _redis.StringGetAsync(_baseKey + key);

            if (!result.HasValue)
                return null;

            var token = JsonConvert.DeserializeObject<PersistedGrant>(result, JsonNetSettings.GetJsonSerializerSettings());
            return token;
        }

        async Task<IEnumerable<PersistedGrant>> IPersistedGrantStore.GetAllAsync(string subjectId)
        {
            var allMembers = await _redis.SetMembersAsync(_baseKey + _subjectKey + subjectId);

            var keysToGet = allMembers.Select(x => (RedisKey)x.ToString()).ToArray();

            var redisTokens = await _redis.StringGetAsync(keysToGet);

            var tokens = new List<PersistedGrant>();

            tokens.AddRange(redisTokens.Select(x => JsonConvert.DeserializeObject<PersistedGrant>(x, JsonNetSettings.GetJsonSerializerSettings())));


            return tokens;
        }



        public async Task RemoveAsync(string key)
        {
            await _redis.KeyDeleteAsync(_baseKey + key);
        }

        public async Task RemoveAllAsync(string subjectId, string clientId)
        {
            var allMembers = await _redis.SetMembersAsync(_baseKey + _subjectKey + _ClientKey + subjectId + clientId);

            var keysToDelete = allMembers.Select(x => (RedisKey)x.ToString()).ToArray();

            await _redis.KeyDeleteAsync(keysToDelete);
        }

        public async Task RemoveAllAsync(string subjectId, string clientId, string type)
        {
            var allMembers = await _redis.SetMembersAsync(_baseKey + _subjectKey + _ClientKey + subjectId + clientId + type);

            var keysToDelete = allMembers.Select(x => (RedisKey)x.ToString()).ToArray();

            await _redis.KeyDeleteAsync(keysToDelete);
        }

    }
}

And this also changes the registration from

builder.Services.AddTransient<ITokenHandleStore, RedisTokenHandleStore>();  
builder.Services.AddTransient<IRefreshTokenStore, RedisRefreshTokenStore>();  

to

builder.Services.AddTransient<IPersistedGrantStore, RedisTokenHandleStore>();  

Fourth

step I needed to take is that cause I had 2 ICustomGrantValidator that interface is now IExtensionGrantValidator

Here is a sample implementation of an GoogleGrantValidator.

using System;  
using System.Net.Http;  
using System.Threading.Tasks;  
using IdentityServer4.Models;  
using IdentityServer4.Validation;  
using Newtonsoft.Json;  
using twee.cloud.auth.Services;

namespace twee.cloud.auth.Validators  
{
    public class GoogleGrantValidator : IExtensionGrantValidator
    {
        private readonly IUserService _userService;

        public GoogleGrantValidator(IUserService userService)
        {
            _userService = userService;
        }

        public async Task ValidateAsync(ExtensionGrantValidationContext context)
        {
            var token = context.Request.Raw.Get("google_token");


            var googleHttpClient = new HttpClient();

            var response = await googleHttpClient.GetAsync($"https://www.googleapis.com/oauth2/v3/tokeninfo?id_token={token}");

            if (!response.IsSuccessStatusCode)
            {
                context.Result = new GrantValidationResult(TokenErrors.InvalidGrant, "invalid_google_token");
                return;
            }

            var googleResponse = JsonConvert.DeserializeObject<GoogleTokenResponse>(await response.Content.ReadAsStringAsync());

            if (!string.IsNullOrEmpty(googleResponse.Sub))
            {
                var user = await _userService.VerifyProvider(googleResponse.Sub, "google");
                // valid credential
                if (user == null)
                {
                    context.Result = new GrantValidationResult(TokenErrors.InvalidGrant, "invalid_google_providerid");
                    return;
                }

                context.Result = new GrantValidationResult(user.Id.ToString(), "google");
                return;
            }
            // custom error message
            context.Result = new GrantValidationResult(TokenErrors.InvalidGrant, "invalid_credential");
        }

        public string GrantType
        {
            get { return "google"; }
        }
    }

    public class GoogleTokenResponse
    {
        public string Sub { get; set; }
        public string Email { get; set; }
    }
}

And also there was a changed IResourceOwnerPasswordValidator so instead of returning an CustomGrantValidationResult you will need to set the context.Result exactly as above.

The registering of .AddCustomGrantValidator<GoogleGrantValidator>() has changed name to .AddExtensionGrantValidator<GoogleGrantValidator>()

comments powered by Disqus