Search code examples
asp.net-coreidentityserver4

Return "ui_locale" back to client


So I know how to make IdentityServer4 app to use culture that the challenging client has. By defining

options.Events = new OpenIdConnectEvents
{
   OnRedirectToIdentityProvider = context =>
   {
      context.ProtocolMessage.UiLocales = "pl-PL";
      return Task.CompletedTask;
   },                     
}

I can make IdentityServer4 to also show me login page in "pl-PL". The trick is however, that I allow users to change the language on the login screen. How can I inform the client that culture info was changed during login? Currently my client does not even show any page, goes directly to Login screen (thus from client app browser is redirected immediately to IdentityServer4 app, where a user can change his/her language).


Solution

  • It seems that this is not a functionality that IdentityServer4 offers (any contradictory comments welcome). So I ended up with using claims to pass the culture information back to my client. So I created a class inheriting from IProfileService so I can load additional claim JwtClaimTypes.Locale to the idToken. However it seems that when it is running, it is in a different context then the user it runs for, so CultureInfo.CurrentCulture is set to a different locale than what I was expecting (for example the UI was set pl-PL but inside profile service, it was set to en-US). So I ended up with creating a InMemoryUserInfo class that is basically a wrapped ConcurrentDictionary that contains my user id and an object that contains user's selected locale. I create entry/update that dictionary, whenever user changes the preferred language or when a user language is delivered from the database. Anyway, that InMemoryUserInfo is then injected into my profile service where it is added as another claim:

    public class IdentityWithAdditionalClaimsProfileService : IProfileService
    {
        private readonly IUserClaimsPrincipalFactory<ApplicationUser> _claimsFactory;
        private readonly UserManager<ApplicationUser> _userManager;
        
        /// <summary>
        /// This services is running in a different thread then UI, so
        /// when trying to obtain CultureInfo.CurrentUICulture, it not necessarily
        /// is going to be correct. So whenever culture is changed,
        /// it is stored in InMemoryUserInfo. Current user's culture will
        /// be included in a claim.
        /// </summary>
        private readonly InMemoryUserInfo _userInfo;
    
        public IdentityWithAdditionalClaimsProfileService(
            IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory,
            UserManager<ApplicationUser> userManager, 
            InMemoryUserInfo userInfo)
        {
            _claimsFactory = claimsFactory;
            _userManager = userManager;
            _userInfo = userInfo;
        }
    
        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var sub = context.Subject.GetSubjectId();
            var user = await _userManager.FindByIdAsync(sub);
            var principal = await _claimsFactory.CreateAsync(user);
    
            var claims = principal.Claims.ToList();
            claims = claims.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList();            
           
            claims.Add(new Claim(JwtClaimTypes.Locale, _userInfo.Get(user.Id).Culture ?? throw new ArgumentNullException()));
    
            context.IssuedClaims = claims;
        }
    
        public async Task IsActiveAsync(IsActiveContext context)
        {
            var sub = context.Subject.GetSubjectId();
            var user = await _userManager.FindByIdAsync(sub);
            context.IsActive = user != null;
        }
    }
    

    Remember to register IProfileService with DI

    services.AddTransient<IProfileService, IdentityWithAdditionalClaimsProfileService>();
    

    Afterwards, in my client's startup, I analyse the claims in OpenIdConnectEvents and set the cookie to culture received from IdentityServer:

    .AddOpenIdConnect("oidc", options =>
    {
        options.Events = new OpenIdConnectEvents 
        {
            OnTicketReceived = context =>
            {
                //Goes through returned claims from authentication endpoint and looks for
                //localization info. If found and different, then new CultureInfo is set.
                string? culture = context.Principal?.FindFirstValue(JwtClaimTypes.Locale);
                if (culture != null && CultureInfo.CurrentUICulture.Name != culture)
                {
                    context.HttpContext.Response.Cookies.Append(
                        CookieRequestCultureProvider.DefaultCookieName,
                        CookieRequestCultureProvider.MakeCookieValue(
                            new RequestCulture(culture, culture)),
                            new CookieOptions 
                            { Expires = DateTimeOffset.UtcNow.AddYears(1) }
                        );
                }
                return Task.CompletedTask;
            };
        }
    });