0

Following on from this question, I ended up using the HttpContext.SignInAsync(string subject, Claim[] claims) overload to pass the selected tenant id as a claim (defined as type "TenantId") after the user selects a Tenant.

I then check for this claim in my custom AccountChooserResponseGenerator class from this question to determine if the user needs to be directed to the Tenant Chooser page or not, as follows:

public override async Task<InteractionResponse> ProcessInteractionAsync(ValidatedAuthorizeRequest request, ConsentResponse consent = null)
        {
            var response = await base.ProcessInteractionAsync(request, consent);
            if (response.IsConsent || response.IsLogin || response.IsError)
                return response;

            if (!request.Subject.HasClaim(c=> c.Type == "TenantId" && c.Value != "0"))
                return new InteractionResponse
                {
                    RedirectUrl = "/Tenant"
                };

            return new InteractionResponse();
        }

The interaction is working and the user gets correctly redirected back to the Client app after selecting a Tenant.

However, on my client, I have the simple:

<dl>
            @foreach (var claim in User.Claims)
            {
                <dt>@claim.Type</dt>
                <dd>@claim.Value</dd>
            }
        </dl>

snippet from the IdentityServer4 quickstarts to show the claims, and sadly, my TenantId claim is not there.

I have allowed for it in the definition of my Client on my IdentityServer setup, as follows:

var client = new Client
                {
... other settings here
                    AllowedScopes = new List<string>
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        IdentityServerConstants.StandardScopes.Email,
                        IdentityServerConstants.StandardScopes.Phone,
                        "TenantId"
                    }
                };

What am I missing in order for this TenantId claim to become visible in my Client application?

EDIT:

Based on @d_f's comments, I have now added TentantId to my server's GetIdentityResources(), as follows:

public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                new IdentityResources.Email(),
                new IdentityResources.Phone(),
                new IdentityResource("TenantId", new[] {"TenantId"})
            };
        }

And I have edited the client's startup.ConfigureServices(IServiceCollection services) to request this additional scope, as follows:

services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
                .AddCookie("Cookies")
                .AddOpenIdConnect("oidc", options =>
                {
                    //other settings not shown

                    options.Scope.Add("TenantId");

                });

And still the only claims displayed on the client by the indicated snippet are: claims

Edit 2: Fixed!

Finally @RichardGowan's answer worked. And that is because (as brilliantly observed by @AdemCaglin) I was using IdentityServer's AspNetIdentity, which has it's own implementation of IProfileService, which kept dropping my custom TenantId claim, despite ALL these other settings).

So in the end, I could undo all those other settings...I have no mention of the TenantId claim in GetIdentityResources, no mention of it in AllowedScopes in the definition of the Client in my IdSrv, and no mention of it in the configuration of services.AddAuthentication on my client.

Shawn de Wet
  • 5,642
  • 6
  • 57
  • 88
  • You've added "TenantId" as AllowedScope for your client, but you've not described that scope. Something like: services.AddIdentityServer() .AddInMemoryIdentityResources(new List { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Email(), new IdentityResource("TenantId", new[] {"TenantId"}) }) – d_f Aug 07 '18 at 16:53
  • and you have to request that additional scope in your client configuration – d_f Aug 07 '18 at 16:57
  • or simply `.AddInMemoryIdentityResources(GetIdentityResources())` and later: `public static List GetIdentityResources() { /*Claims automatically included in OpenId scope*/ var openIdScope = new IdentityResources.OpenId(); openIdScope.UserClaims.Add("TenantId"); /* Available scopes*/ return new List{openIdScope, new IdentityResources.Profile(), new IdentityResources.Email(), /*etc*/}; }` – d_f Aug 07 '18 at 17:48
  • @d_f thanks for your feedback...please see my edits indicating changes made to my setup...but still no TenantId claim on the client side? – Shawn de Wet Aug 08 '18 at 02:43
  • Add `AlwaysIncludeUserClaimsInIdToken` setting to your client's configuration in IdSrv. – d_f Aug 08 '18 at 11:53

1 Answers1

0

You will need to provide and register an implementation of IProfileService to issue your custom claim back to the client:

public class MyProfileService : IProfileService {
    public MyProfileService() {
    }

    public Task GetProfileDataAsync(ProfileDataRequestContext context) {
        // Issue custom claim
        context.IssuedClaims.Add(context.Subject.Claims.First(c => c.Type == 
               "TenantId"));
        return Task.CompletedTask;
    }

    public Task IsActiveAsync(IsActiveContext context) {
        context.IsActive = true;
        return Task.CompletedTask;
    }
}
Richard
  • 1,534
  • 1
  • 12
  • 16
  • That's not necessary. The DefaultProfileService implementation already does that job. Custom profile service is amid for filtering or for issuing totally new claims not yet within the Subject. – d_f Aug 08 '18 at 12:04
  • As I mentioned, the default `IProfileService` implementation makes the same thing, since it has `context.AddRequestedClaims(context.Subject.Claims);` inside. Seems, you performed some additional change or fix to make it work... – d_f Aug 08 '18 at 16:27
  • 1
    @d_f Probably OP uses identityserver with identity and this library has own iprofileservice implementation. You can see code https://github.com/IdentityServer/IdentityServer4.AspNetIdentity/blob/dev/src/ProfileService.cs – adem caglin Aug 08 '18 at 18:34
  • @ademcaglin wow what a brilliant observation...I was not aware of that either! But that is exactly it. – Shawn de Wet Aug 09 '18 at 03:21
  • @ademcaglin, I agree, there are too many possibilities without seen the whole solution... that's why I've not rolled out my own answer... the answer should be complete : ) – d_f Aug 09 '18 at 10:59
  • @ShawndeWet and now I guess so tough replacing the `ProfileService` from Identity is a step in a wrong direction (if you really need that Identity, and don't plan to implement your own UserStore). I could suggest to take the existing implementation as a base -- that would be more polite and straightforward for later support. At least... But more proper way could be extending UserClaimsFactory form the same package, or UserManager... I don't know, that's your project : ) – d_f Aug 09 '18 at 12:16