Identityserver4: [Question]IdentityServer4 user impersonation

Created on 28 Feb 2017  路  30Comments  路  Source: IdentityServer/IdentityServer4

Hi! Thanks for idSrv

Please consider create an example for user impersonation on idSrv4. I find examples how to impersonate user on idSrv3 (using arc_values and PreAuthenticateAsync) , but can't find examples for idSrv4.

Thanks

question

Most helpful comment

It's simple enough to derive a class from AuthorizeInteractionResponseGenerator. You will need to provide a constructor to pass on the initialisation parameters to the base class. Then override the ProcessInteractionAsync method. In there you can check the client and I presume acr values:

```c#
public override async Task ProcessInteractionAsync(ValidatedAuthorizeRequest request, ConsentResponse consent = null)
{
if (request?.Client?.ClientId == "MySpecialClientId")
{
// TODO: Do some other behind the scenes check

   var claims = new[]
   {
      new Claim(JwtClaimTypes.Name, "Fred Blogs"),
      new Claim(JwtClaimTypes.FamilyName, "Blogs"),
      new Claim(JwtClaimTypes.GivenName, "Fred"),
      new Claim(JwtClaimTypes.Email, "[email protected]"),
   };

    var newPrincipal = IdentityServerPrincipal.Create("fred.blogs", "Fred Blogs", claims);
    request.Subject = newPrincipal;

    return new InteractionResponse();
}

return await base.ProcessInteractionAsync(request, consent);

}
`` You can register this new class inConfigureServicesmethod ofStartupby adding a call toAddAuthorizeInteractionResponseGenerator()to all your other calls against the result ofservices.AddIdentityServer()`

However, this doesn't work just yet. It throws an exception as I said above:

ArgumentNullException: Value cannot be null. Parameter name: principal deep in the DefaultAuthenticationManager

I haven't had time to investigate why and any pointers would be appreciated.

All 30 comments

We don't have time for that right now, but if you want to put a sample or blog post together then let us know and we can link to it.

Unfortunately, I don't know what I need to use instead PreAuthenticateAsync in idSrv4. On client side I can use OnRedirectToIdentityProvider of OpenIdConnect middleware to add acr_values, such as impersonate:user to auth request, but can't find any info how to receive acr_values and make AuthenticateResult on server side. Maybe anyone from community can help me?

Thanks.

We dont' have much docs on it now, but it's possible. PreAuth doesn't exist in IS4 because the UI is entirely up to you. You'd need to check acr_values in a custom IAuthorizeInteractionResponseGenerator (or better in a AuthorizeInteractionResponseGenerator-derived class).

brockallen, thanks for answer

Trying create custom AuthorizeInteractionResponseGenerator-derived class and use it with AddAuthorizeInteractionResponseGenerator, I can check acr_values for impersonate user name but can't find any way to authorize user without UI interaction. I need non-interactive way to impersonate user, just sign in without password and login prompt and return to returnUrl.

@brockallen would you suggest to replicate functionality that might have previously been put in PreAuthenticateAsync to provide a feature such as https://github.com/IdentityServer/IdentityServer3/issues/2385 (i.e. to automatically authenticate a user) that the best approach would be to override ProcessInteractionAsync in AuthorizeInteractionResponseGenerator? I'm sure similar functionality could be put in the UI, but that would require an extra redirect of the user's browser.

I have tried overriding ProcessInteractionAsync and replacing request.Subject with a new Principal using the IdentityServerPrincipal.Create() method then returning a new InteractionResponse() rather than setting IsLogin = true or IsConsent = true... it didn't quite work, blowing up with:

ArgumentNullException: Value cannot be null. Parameter name: principal deep in the DefaultAuthenticationManager

Yes, that'd work (modulo any other pieces in IS that assume you have cookies).

I don't suppose you have an idea about what else I might need to set in order to keep the rest of the authorization process happy?

I'm also trying to achieve impersonation, would appreciate any help how to do this, setting acr values, writkng a AuthorizeInteractionResponseGenerator derived class which checks for a specific acr value and then how to "login" as the requested user.

How would my OIDC clients/resource servers know that the logged on user actually impersonates another user?

How can I restrict only certain client and users from sending these acr values for impersonating other users?

It's simple enough to derive a class from AuthorizeInteractionResponseGenerator. You will need to provide a constructor to pass on the initialisation parameters to the base class. Then override the ProcessInteractionAsync method. In there you can check the client and I presume acr values:

```c#
public override async Task ProcessInteractionAsync(ValidatedAuthorizeRequest request, ConsentResponse consent = null)
{
if (request?.Client?.ClientId == "MySpecialClientId")
{
// TODO: Do some other behind the scenes check

   var claims = new[]
   {
      new Claim(JwtClaimTypes.Name, "Fred Blogs"),
      new Claim(JwtClaimTypes.FamilyName, "Blogs"),
      new Claim(JwtClaimTypes.GivenName, "Fred"),
      new Claim(JwtClaimTypes.Email, "[email protected]"),
   };

    var newPrincipal = IdentityServerPrincipal.Create("fred.blogs", "Fred Blogs", claims);
    request.Subject = newPrincipal;

    return new InteractionResponse();
}

return await base.ProcessInteractionAsync(request, consent);

}
`` You can register this new class inConfigureServicesmethod ofStartupby adding a call toAddAuthorizeInteractionResponseGenerator()to all your other calls against the result ofservices.AddIdentityServer()`

However, this doesn't work just yet. It throws an exception as I said above:

ArgumentNullException: Value cannot be null. Parameter name: principal deep in the DefaultAuthenticationManager

I haven't had time to investigate why and any pointers would be appreciated.

Sorry @brockallen I don't understand your comment about "modulo" or do you mean this same technique could be used to workaround all other parts of the process that would normally use cookies to work.

It just means that when I've done something like that before there are other parts of IS that expect your auth cookie to be present. I don't know if you have one or not, so it may or may not work. If not, then you might need to replace those other parts as well.

I've spent a bit more time on this but I don't seem to be moving forward. I tried calling

AuthenticationProperties props = null;
await _httpContextAccessor.HttpContext.Authentication.SignInAsync("fred.blogs", "Fred Blogs", props, claims);

having added IHttpContextAccessor as one of the services to be injected via the constructor. Still no joy.

Perhaps @brockallen the ability to pre authenticate could be added as a feature request? I know it is probably only of use for people wanting to support SSO from a legacy application, but it does help when gradually transitioning from such an application to a new IdentityServer based world!

In the meantime, I think my workaround will have to be something in my LoginController that doesn't much the same as above and assume the extra redirect will not take much time.

Thank you for your help.

I did get this working for my situation... i.e. I want the user to have the IdentityServer auth cookie put into their browser so that when they are redirected to a protected page in another application they are already logged in. I'm not going to use this workaround going forward because it is a nasty hack.... but here it is:

  • I added a IHttpContextAccessor httpContextAccessor to the constructor of my class derived from AuthorizeInteractionResponseGenerator and stored it away

  • In my override of ProcessInteractionAsync I checked if the client was the one that I wanted to allow to circumvent the login process

  • If it was that client I checked for a one time ticket passed via a back channel with an ACR value passed within the request

  • If the ACR value matched a ticket I called SignInAsync as shown above

  • I then created a new InteractionResponse object with its RedirectUrl set to the Url passed in by the client (request.RedirectUri) having first checked this was one of the client's allowed URIs.

This works... if I now go to another application and navigate to a protected page, I get logged in automatically.

HOWEVER... it certainly smells bad! So until there is an official way to do this, I think I'll stick to having an extra redirect to the login page.

Do you need the signin cookie, or not? It's what provides SSO. Just something to think about.

@brockallen doesn't a call to HttpContext.Authentication.SignInAsync create the signin cookie? The SSO seems to be working after I have called that method.

I still don't really want to use this approach of overriding ProcessInteractionAsync and using the RedirectUri as it seems a bit too much of a hack. It would be great if IdSvr4 supported this functionality as IdSvr3 did.

We made the default authorize interaction generator public for doing these sorts of things. If your company wants a formal feature added, then please contact us for consulting on it.

What the correct way to authorize user in ProcessInteractionAsync? I use _httpContextAccessor.HttpContext.Authentication.SignInAsync, as pierslawson recommend, but with no luck. Identity Server impersonate user, but when I return to my MVC site, I see old user claims.

What the correct way to authorize user in ProcessInteractionAsync

The authenticated user is on the ValidatedAuthorizeRequest.Subject

Did you manage to get this working?

@leastprivilege as mentioned above I got this working but it seemed too much of a bodge to make me comfortable that it was the right way ahead. @brockallen's comment that there were probably lots of other places that I should consider making changes put me off trusting my workaround... and @odysseus1973's problems are probably an indication of the hidden dangers. I'm afraid I ran out of time to dig deeper, sorry! My current approach is to allow the natural flow to carry on but have code within the AccountController to automatically log the user in. So an extra browser redirect happens, but at least I'm generally following the right flow.

That's something that needs better documentation. Thanks for the update.

I am having the same problem as @pierslawson. Getting ArgumentNullException: Value cannot be null. Parameter name: principal. Does anyone manage to get this working?

hi all,
@sanjaygulati did you solve it ? if you return InteractionResponse with a RedirectUrl , it's working, but @pierslawson 's doubts are correct I think.
@leastprivilege any documentation update coming ? ;)
Does openID Connect spec have any clues ?
Regards

you can search for impersonation in OpenID; you will find it is a bad word.
The client is granted limited rights of the user to access resources.
Impersonation in an open protocol just doesn't make any sense.

Is there anything further on this? I'm having the same problem. @leastprivilege is there any clear instructions on how to ensure the user is set correctly when using the interaction interface?

Addtionally, when looking through the stack trace in the error, it points to the following ID classes that don't seem to exist in the git repo: DefaultClientSessionService.

I'm using identity server 1.5.2.

Any help would be greatly appreciated.

I have been working with this for a while starting with @pierslawson's code. There was however an issue with the returnUrl. The case is that the original website get a call to a protected url. This then redirects to IDS4 and after a successful login should redirect back to the website with a query parameter that incudes the returnUrl. In @pierslawson's example this queryparameter was lost. This is because it is embedded in the OpenIdConnect.AuthenticationProperties. To solve this I added a redirect in the end of the ProcessInteractionAsync. I also added code to set the SSO cookie at IDS4 and made a check to only run the code for users that where not authenticated.
```c#
public override async Task ProcessInteractionAsync(ValidatedAuthorizeRequest request, ConsentResponse consent = null)
{
//
// https://github.com/IdentityServer/IdentityServer4/issues/853
//
var isAuthenticated = _httpContextAccessor.HttpContext.User.Identity.IsAuthenticated;
var userId = request.GetAcrValues().FirstOrDefault(a => a.StartsWith("impersonatedUserId"));
if (isAuthenticated || key == null)
{
return await base.ProcessInteractionAsync(request, consent);
}
int userId;
string userName;
ICollection claims;
try
{
// Get or create claim, userId and userName
}
catch (LoginException e)
{
Log.Error($"({e.Id} " + e);
return new InteractionResponse()
{
Error = e.Message
};
}
await _events.RaiseAsync(new UserLoginSuccessEvent(user.Name, user.UserId.ToString(), user.Name));
var props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
// Using IDP4 extension methiod to signin, it is in the Microsoft.AspNetCore.Http namespace and AuthenticationManagerExtensions class
await _httpContextAccessor.HttpContext.SignInAsync(user.UserId.ToString(), props, claims.ToArray());
return new InteractionResponse()
{
// Redirect to controller
RedirectUrl = "/impersonae/login"
};
}

And the controller
```c#
[HttpGet]
public async Task<IActionResult> Login(string returnUrl)
{
    // Not shure if all this is needed, copy from AccountControler
    var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
    if (context != null)
    {
        if (await _clientStore.IsPkceClientAsync(context.ClientId))
        {
            return View("Redirect", new RedirectViewModel {RedirectUrl = returnUrl});
        }

        return Redirect(returnUrl);
    }

    if (Url.IsLocalUrl(returnUrl))
    {
        return Redirect(returnUrl);
    }

    if (string.IsNullOrEmpty(returnUrl))
    {
        return Redirect("~/");
    }

    // user might have clicked on a malicious link - should be logged
    throw new Exception("invalid return URL");
}
  • I then created a new InteractionResponse object with its RedirectUrl set to the Url passed in by the client (request.RedirectUri) having first checked this was one of the client's allowed URIs.

Hello

I did get this working for my situation... i.e. I want the user to have the IdentityServer auth cookie put into their browser so that when they are redirected to a protected page in another application they are already logged in. I'm not going to use this workaround going forward because it is a nasty hack.... but here it is:

*

@pierslawson , Can you please help with the sample code here. I also wanted to do user impersonation with back channel login.

@Prince269090 I recently had a need to automatically log a user in when a temporary token was specified in the query string. This would be very similar to what I would do for impersonation functionality. I used snippets of the above code solutions for my final solution:

Here's what worked for me:

public class MyAuthorizeInteractionResponseGenerator : AuthorizeInteractionResponseGenerator
{
    private readonly AccountDbContext _accountDbContext;
    private readonly UserManager<AccountUser> _userManager;
    private readonly SignInManager<AccountUser> _signInManager;
    private readonly HttpContext _httpContext;
    private readonly ILogger _logger;

    public MyAuthorizeInteractionResponseGenerator(
        AccountDbContext accountDbContext,
        ISystemClock clock,
        ILogger<MyAuthorizeInteractionResponseGenerator> logger,
        IConsentService consent,
        IProfileService profile,
        UserManager<AccountUser> userManager,
        SignInManager<AccountUser> signInManager,
        IHttpContextAccessor httpContextAccessor) : base(clock, logger, consent, profile)
    {
        _accountDbContext = accountDbContext;
        _userManager = userManager;
        _signInManager = signInManager;
        _httpContext = httpContextAccessor.HttpContext;
        _logger = logger;
    }

    public override async Task<InteractionResponse> ProcessInteractionAsync(ValidatedAuthorizeRequest request, ConsentResponse consent = null)
    {
        var queryToken = _httpContext.Request.Query["guest_token"].ToString();
        if (!string.IsNullOrWhiteSpace(queryToken))
        {
            // Somehow get the user record that you want to log in.  In this case,
            // it is via a temporary guest token that we've embedded in the call to the authorize endpoint.
            var token = await (from gt in _accountDbContext.GuestTokens
                               where gt.Token == queryToken
                               select gt).SingleOrDefaultAsync();
            if (token?.Expiration > DateTime.UtcNow)
            {
                var user = await (from ac in _accountDbContext.Users
                                  where ac.Id == token.UserId
                                  select ac).SingleAsync();

                // Sign the user in using ASP.NET identity to generate the appropriate cookie
                await _signInManager.SignInAsync(user, false);

                // Create a claims principal to pass to the ID4 signin extension method
                var cp = await _signInManager.CreateUserPrincipalAsync(user);

                // Sign the user in using the ID4 signin extension method to generate appropriate claims.
                // If you don't specificy the scheme, you may get complaints about the idp claim not being populated
                // There are other overloads here that may make sense for you.
                await _httpContext.SignInAsync(IdentityConstants.ApplicationScheme, cp, new AuthenticationProperties
                {
                    IsPersistent = true,
                });

                // Set the request subject to the claims principal so that the base.ProcessInteractionAsync
                request.Subject = cp;

                // If you are impersonating a user, you may want to add additional claims so that you can
                // revert back to your previous user somehow.

                // Execute the base class implementation now that we've populated the claims principal.  It
                // should now run through the registered IProfileService in case you've added any custom claims
                // generation.
                var guestInteractionResponse = await base.ProcessInteractionAsync(request, consent);
                return guestInteractionResponse;
            }
        }

        // This is the normal flow where you're not manually logging in a user or doing any
        // type of impersonation

        // Do more checks here for any other custom behavior if you want
        return await base.ProcessInteractionAsync(request, consent);
    }

    /*
    ....
    */
}

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

agilenut picture agilenut  路  3Comments

brockallen picture brockallen  路  3Comments

user1336 picture user1336  路  3Comments

createroftheearth picture createroftheearth  路  3Comments

ekarlso picture ekarlso  路  3Comments