4

In my IdentityServer4 project I implement IProfileService. The IsActiveAsync method is invoked a number of times after the human-user has successfully authenticated through the login web-page when using Implicit and Hybrid flows.

I've noticed it's invoked when context.Caller is one of these values:

  • AuthorizeEndpoint (with the User's Claims)
  • AuthorizationCodeValidation (with the User's Claims)
  • AccessTokenValidation (without the User's Claims)

Due to a bug my code set context.IsActive = false - and when this happened the web-browser which was used to get to the login-page was just redirected back to the login page with no error message or reason information. Users would be confused why they had successfully authenticated but prompted to login again. No new querystring parameters were added either.

The IdentityServer4 logs do display the reason:

[23:16:40 Information] IdentityServer4.ResponseHandling.AuthorizeInteractionResponseGenerator Showing login: User is not active

Now, supposing that my IsActive = false code was not a bug, but was actually by-design (because, for example, the user's account really was disabled in the microseconds between different OAuth/OpenIDConnect HTTP requests), in which case how can I ensure this message is presented to the user and/or client software?

Simple Code
  • 2,354
  • 2
  • 27
  • 56
Dai
  • 141,631
  • 28
  • 261
  • 374

1 Answers1

3

After some investigation I've decided to update the answer.

First of all, context.IsActive is used to indicate whether GetProfileDataAsync should be executed.

The 'problem' is that the client starts multiple calls to the ProfileService to retrieve information. These seperate calls are initiated by the client and follow after the user did login.

For a direct response you can add code to the Login method. When the user is not active you can respond with an error message. I think this is the best place to check, because it'll prevent a lot of actions.

The following snippet is taken from one of the samples of IdentityServer. It handles the Login request.

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model, string button)
{
    if (ModelState.IsValid)
    {
        var result = await _signInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberLogin, lockoutOnFailure: true);
        if (result.Succeeded)
        {
            var user = await _userManager.FindByNameAsync(model.Username);

            // Assume user has property IsActive for this example.
            // You can implement this anyway you like.
            if (user.IsActive)
            {
                ...
            }
        }

        ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage);
    }

    // something went wrong, show form with error
    var vm = await BuildLoginViewModelAsync(model);
    return View(vm);
}

You can add any message you like using ModelState.AddModelError.

Now suppose the user did login and was deactivated later, then the ProfileService will not return claims. Instead it will lead to a 401: User is not active.

The client will then throw an exception, because the request has to be succesful: HttpResponseMessage.EnsureSuccessStatusCode.

Luckily there is an option to handle the exception. And that is to implement oidc events. This is just a simple example:

services.AddOpenIdConnect("oidc", "Open Id connect", options =>
{
    options.Events = new OpenIdConnectEvents()
    {
        // When a user is not active this will result in a 401
        OnAuthenticationFailed = (context) =>
        {
            // Clear the exception, otherwise it is re-thrown after this event.
            context.HandleResponse();
            // Handle the exception, e.g. redirect to an error page.
            context.Response.Redirect($"LoginError/?message={context.Exception.Message}");

            return Task.FromResult(0);
        },
    }
}

This may be what you are looking for.

  • But what if there were some circumstances that caused `IsActive` to indicate false *after* the user had already successfully authenticated in the `Login` action? – Dai Sep 01 '18 at 07:37
  • @Dai Does this answer your questions? Or is something missing from the answer? –  Sep 07 '18 at 09:12
  • Thank you for checking-in: I've switched to an alternative project for now and I haven't actually gotten around to working on this problem yet, but I'll be back on that project and get to try your approach and see how it fares before I can mark it as accepted, but I'll upvote you anyway :) – Dai Sep 07 '18 at 09:26