0

Inspired by an article on custom claims, I've added a tenant id custom claim to my Identity server sign in process as follows:

using System;
using System.Security.Claims;
using System.Threading.Tasks;
using MyNamespace.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using MyNamespace.Data;
using MyNamespace.Constants;

namespace MyNamespace.Factories
{

    public class TenantClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser>
    {
        public TenantClaimsPrincipalFactory(
            UserManager<ApplicationUser> userManager,
            IOptions<IdentityOptions> optionsAccessor)
            : base(userManager, optionsAccessor) {
        }

        // TODO: Remove hard binding to application db context
        protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user) {
            var identity = await base.GenerateClaimsAsync(user);
            var tenantId = ApplicationDbContext.DefaultTenantId;
            if (user.TenantId != Guid.Empty) {
                tenantId = user.TenantId;
            }
            identity.AddClaim(new Claim(CustomClaimTypes.TenantId, tenantId.ToString()));
            return identity;
        }
    } 

}

The claims generating method is executed at login and claims are added to the identity, so this part seems ok. Later I try to read out the claim later in my tenant provider service as follows

using System;
using MyNamespace.Data;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
using System.Linq;
using MyNamespace.Constants;

namespace MyNamespace.Services
{

    public interface ITenantProvider
    {
        Guid GetTenantId();
    }

    public class TenantProvider : ITenantProvider
    {
        private IHttpContextAccessor _httpContextAccessor;

        public TenantProvider(IHttpContextAccessor httpContextAccessor
        {
            _httpContextAccessor = httpContextAccessor;
        }

        // TODO: Remove hard binding to application db context
        public Guid GetTenantId()
        {
            var userId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
            var user = _httpContextAccessor.HttpContext.User;
            var tenantId = _httpContextAccessor.HttpContext.User.FindFirst(CustomClaimTypes.TenantId).Value;    

            Guid tenantGuid = ApplicationDbContext.DefaultTenantId;
            Guid.TryParse(tenantId, out tenantGuid);

            return tenantGuid;
        }
    }

}

As far as I understand, however, the claim identified by CustomClaimTypes.TenantId is not automatically mapped by the Identity server. My question is this: how can I map

options.ClaimActions.MapUniqueJsonKey(CustomClaimTypes.TenantId, CustomClaimTypes.TenantId);

from Startup.cs where I add the Identity server the my dependencies:

services.AddAuthentication()
            .AddIdentityServerJwt();
conciliator
  • 6,078
  • 6
  • 41
  • 66

1 Answers1

2

So, in the end I ended up with a different solution than what I sought originally. Instead of mapping the claims as created by the factory, I came across another post here at StackOverflow. Basically, what I did was the following. I implemented the following ProfileService

namespace MyNamespace.Services
{

    public class ProfileService : IProfileService
    {
        protected UserManager<ApplicationUser> _userManager;

        public ProfileService(UserManager<ApplicationUser> userManager)
        {
            _userManager = userManager;
        }

        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var user = await _userManager.GetUserAsync(context.Subject);

            var claims = new List<Claim>
            {
                new Claim(CustomClaimTypes.TenantId, user.TenantId.ToString()),
            };

            context.IssuedClaims.AddRange(claims);
        }

        public async Task IsActiveAsync(IsActiveContext context)
        {
            var user = await _userManager.GetUserAsync(context.Subject);

            context.IsActive = (user != null) && user.IsActive;
        }
    }

}

Then, I added the service in the DI Container at Configure:

services.AddIdentityServer()
                .AddApiAuthorization<ApplicationUser, ApplicationDbContext>()
                .AddProfileService<ProfileService>();

services.AddAuthentication()
                .AddIdentityServerJwt();

So, I still have a good time letting AddIdentityServerJwt setting up IdentityServer4, while getting my claims at the same time.

conciliator
  • 6,078
  • 6
  • 41
  • 66
  • I have started using the same preview example, the client app logs in but I cant seem to get any Identity info (user name, claims etc) How do I reference the ProfileService and what do I pass in for the context? – Mark Redman Jul 26 '19 at 09:12
  • 1
    @MarkRedman are you trying to access the identity info from the server side? If you want to access the user session from one of your controllers, you could use `var userId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;`. Be sure to pass in `IHttpContextAccessor httpContextAccessor` in your controller constructor first, retaining a reference to `httpContextAccessor` in a private field. – conciliator Jul 26 '19 at 09:54
  • I have no claims coming through (and manually added to AspNetUserClaims to test) I just know the user is AUthenticated, this is the default implementation of: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-api-authorization?view=aspnetcore-3.0, using react. – Mark Redman Jul 26 '19 at 14:47
  • Hmm - I don't know, man. Maybe it'd be easier to assist you if you wrote a separate question on the issue you're facing, telling us how you manually added to AspNetUserClaims and so on. Give me a heads up, and I'll look into it. ;) – conciliator Jul 26 '19 at 15:52
  • I will do some more research and see if I am missing any additional configuration... – Mark Redman Jul 26 '19 at 15:58