20

Sadly documentation on the implementation of a custom AuthorizeInteractionResponseGenerator in IdentityServer4 is sorely lacking.

I'm trying to implement my own AuthorizeInteractionResponseGenerator because I need a further step of user interaction (after authentication). My scenario is that a single identity (email) can be associated with multiple tenants. So after logon, I need the user to be presented with a list of associated tenants, so that they can choose one.

I have evaluated the source code, and have come up with the the following custom AuthorizeInteractionResponseGenerator:

public class AccountChooserResponseGenerator : AuthorizeInteractionResponseGenerator
    {
        public AccountChooserResponseGenerator(ISystemClock clock, 
            ILogger<AuthorizeInteractionResponseGenerator> logger, 
            IConsentService consent, IProfileService profile) 
            : base(clock, logger, consent, profile)
        {
        }

        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;

            return new InteractionResponse
            {
                RedirectUrl = "/Organization"
            };
        }
    }

It inherits from the base AuthorizeInteractionResponseGenerator built into IdentityServer4, so that the standard Logon and Consent pages can show. This happens, and then the user is correctly redirected to the /Organization url to select an organization (tenant).

But what then? With the lack of documentation and examples, I'm really struggling to figure out the following two questions:

1) How do I now, having selected a Tenant, indicate to my custom AccountChooserResponseGenerator that my interaction is complete, and that the user can now be redirected back to the Client?

Edit:

Answer to 1: To indicate that the interaction is complete, you I have to return an empty new InteractionResponse(). In my case, a check for the existence of the TenantId claim sufficed, as follows:

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

            return new InteractionResponse();

2) And how can I get information about the selected Tenant to be added to the identity token that IdentityServer4 passes back to the Client?

Edit: Answer to 2: In the Controller Action method that gets executed after selecting a Tenant, I called :

 await HttpContext.SignInAsync(User.Claims.Single(r=> r.Type == "sub").Value,
                new System.Security.Claims.Claim("TenantId", tenant.Id.ToString()));

            return Redirect(ReturnUrl);

...which is an IdentityServer4-provided Extension to HttpContext.

Maulik Modi
  • 183
  • 3
  • 18
Shawn de Wet
  • 5,642
  • 6
  • 57
  • 88
  • 2
    We achieved something similar using partial login. You can get an example from this question: https://stackoverflow.com/questions/40609585/how-to-do-multiple-step-login-in-identityserver4 – mai Sep 26 '19 at 09:24
  • We also did something similar, once for checking whether we need to force a password change and again for checkin if the tenant is uniquely determined or whether we need to ask the user to select. The approach was to create a method `public async Task OnGetFinishLogin(string returnUrl = null)` in `Login.cshtml.cs` which checks these extra conditions and recirects appropriately, and if nothing needs doing then redirects back to the client application. – Richard Barraclough May 17 '22 at 16:04
  • Whenever a page _thinks_ it's completed the login it should redirect to this new action. If extra information gathered from the user needs to be added as a claim then it can be added as a `UserClaim` (and used on the next login to pre-populate forms) -- being careful not to accumulate duplicates. – Richard Barraclough May 17 '22 at 16:07
  • Note that the client that redirects to IdentityServer for login can add parameters for IdentityServer to use. They come in like this: `string tenantSubdomain = (await _interaction.GetAuthorizationContextAsync(returnUrl))?.Parameters["tenant"]`. – Richard Barraclough May 17 '22 at 16:08

1 Answers1

1

To implement a custom interaction response generator in Identity Server 4 and add information about the selected tenant to the identity token, you can inherit from the base AuthorizeInteractionResponseGenerator and override the ProcessInteractionAsync method. You can also use the IProfileService interface provided by Identity Server 4 to add the tenant information as a claim to the user's identity before the token is issued.

Steps to implement a custom interaction response generator:

Inherit from the base AuthorizeInteractionResponseGenerator and override the ProcessInteractionAsync method:

// Example implementation of ProcessInteractionAsync
public override async Task<InteractionResponse> ProcessInteractionAsync(ValidatedAuthorizeRequest request, ConsentResponse consent = null)
{
    // Your custom logic here

    // Indicate that the interaction is complete
    return new InteractionResponse();
}

Add information about the selected tenant to the identity token:

// Example implementation of IProfileService
public class CustomProfileService : IProfileService
{
    private readonly UserManager<ApplicationUser> _userManager;

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

    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        var user = await _userManager.GetUserAsync(context.Subject);
        if (user != null)
        {
            var tenantIdClaim = user.Claims.FirstOrDefault(c => c.Type == "TenantId");
            if (tenantIdClaim != null)
            {
                context.IssuedClaims.Add(new Claim("tenant_id", tenantIdClaim.Value));
            }
        }
    }

    public Task IsActiveAsync(IsActiveContext context)
    {
        return Task.CompletedTask;
    }
}

Register the CustomProfileService with Identity Server 4 in your Startup.cs file:

services.AddTransient<IProfileService, CustomProfileService>();

Tips:

  1. Always call base class methods first when overriding methods from IdentityServer4.
  2. Check the request object for details on the current flow.
  3. Log DEBUG level logs to stdout to trace the flow through your custom logic.
  4. Use the IConsentService and IProfileService injected into your generator to access user profiles and consent data.
  5. Check the Identity Server build instructions for compiling the source, as it makes debugging much easier!
Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77