Search code examples
asp.net-core-2.0identityserver4

Getting Windows/Active Directory groups as role claims with Identity Server 4


I have got a basic Identity Server setup as per the UI sample project instructions on GitHub. I have it set it up to use Windows authentication with our on site AD. This is working beautifully.

My issue is with adding the users AD groups to the claims. As per the sample project I have enabled the IncludeWindowsGroups option. Which seems to be adding the claims to the ClaimsIdentity. However, on my MVC client, when I print out the claims I only ever get the same 4. They are sid, sub, idp and name. I have tried adding other claims but I can never get any others to show up.

I have the following as my Client Setup:

return new List<Client>
        {
            // other clients omitted...

            // OpenID Connect implicit flow client (MVC)
            new Client
            {
                ClientId = "mvc",
                ClientName = "MVC Client",
                AllowedGrantTypes = GrantTypes.Implicit,

                // where to redirect to after login
                RedirectUris = { "http://localhost:5002/signin-oidc" },

                // where to redirect to after logout
                PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },

                AllowedScopes = new List<string>
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile
                },

                RequireConsent = false
            }
        };

Hopefully I am just missing something simple but I am struggling for ideas now, so any pointers would be much appreciated.


Solution

  • I managed to get this working with a few changes, beyond setting IncludeWindowsGroups = true in the IdentityServer4 project. Note that I downloaded the IdentityServer4 UI quickstart as of the 2.2.0 tag

    Per this comment in GitHub, I modified ExternalController.cs in the quickstart UI:

    // this allows us to collect any additonal claims or properties
    // for the specific prtotocols used and store them in the local auth cookie.
    // this is typically used to store data needed for signout from those protocols.
    var additionalLocalClaims = new List<Claim>();
    
    var roleClaims = claims.Where(c => c.Type == JwtClaimTypes.Role).ToList();
    if (roleClaims.Count > 0)
    {
        additionalLocalClaims.AddRange(roleClaims);
    }
    

    I then created a profile service to copy the claims from Windows Auth into the token being sent back:

    public class ProfileService : IProfileService
    {
        private readonly string[] _claimTypesToMap = {"name", "role"};
    
        public Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            foreach (var claimType in _claimTypesToMap)
            {
                var claims = context.Subject.Claims.Where(c => c.Type == claimType);
                context.IssuedClaims.AddRange(claims);
            }
    
            return Task.CompletedTask;
        }
    
        public Task IsActiveAsync(IsActiveContext context)
        {
            context.IsActive = true; //use some sort of actual validation here!
            return Task.CompletedTask;
        }
    }
    

    and registered with IdentityServer4 in Startup.cs

     services
        .AddIdentityServer()
        .AddDeveloperSigningCredential()
        .AddInMemoryIdentityResources(StaticConfig.GetIdentityResources())
        .AddInMemoryApiResources(StaticConfig.GetApiResources())
        .AddInMemoryClients(StaticConfig.GetClients())
        .AddTestUsers(StaticConfig.GetUsers())
        .AddProfileService<ProfileService>();
    

    In my client config in IdentityServer4, I set user claims to be included in the Id token. I found that if I tried to map the claims in the callback to UserInfo, that context was lost in IdentityServer4, so the claims wouldn't map.

    public static class StaticConfig
    {
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                  ...
                  AlwaysIncludeUserClaimsInIdToken = true,
                  ...
                }
             }
        }
    }
    

    Finally, in Startup.cs for the client website, I did not setup the UserInfo callback; I just made sure that my name and role claims were mapped. Note that if your profile service returns any other claim types, you need to manually map them with a call to a helper method on options.ClaimActions.

    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
    services.AddAuthentication(options =>
    {
        options.DefaultScheme = "Cookies";
        options.DefaultChallengeScheme = "oidc";
    })
    .AddCookie("Cookies")
    .AddOpenIdConnect("oidc", options =>
    {
        options.SignInScheme = "Cookies";
    
        options.Authority = "http://localhost:5000";
        options.RequireHttpsMetadata = false;
    
        options.ClientId = "mvc";
        options.ClientSecret = "secret";
        options.SaveTokens = true;
    
        options.ResponseType = "code id_token";
    
        options.TokenValidationParameters = new TokenValidationParameters
        {
            NameClaimType = "name",
            RoleClaimType = "role"
        };
    
        //map any other app-specific claims we're getting from IdentityServer
        options.ClaimActions.MapUniqueJsonKey("someotherclaimname", "someotherclaimname");
    };