DuendeSoftware / Support

Support for Duende Software products
21 stars 0 forks source link

Handling User Data Between Interactions #501

Closed billcunnien closed 1 year ago

billcunnien commented 1 year ago

Duende 6.1.1 .NET 6

I am trying to setup various interactions during a login (e.g. 2FA) and I would like to pass some data from the successful login validation to these downstream interactions. What is the best way within IS to do this?

For example, after validation I get a response object. In that response object I have several flags - one of them is SecurityCodeValidationRequired. If that is true, I want to initiate the 2FA interaction. Also in that object is a list of contacts (i.e. phone numbers, emails). I would like to list those contacts for the user to select.

Thanks! Bill

josephdecock commented 1 year ago

You can use any mechanism that you like to persist data across these pages. ASP.NET gives you lots of options: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/app-state?view=aspnetcore-7.0. The only requirements that IdentityServer puts on you is that you ultimately need to sign the user in (call SignInAsync), and then redirect back to the returnUrl, which takes you back to the IdentityServer middleware and ultimately produces a protocol response.

billcunnien commented 1 year ago

I created a CustomAuthorizeInteractionResponseGenerator based on the AuthorizeInteractionResponseGenerator. Isn't this the place where we would want the redirects happening? Here is my example:

protected override async Task<InteractionResponse> ProcessLoginAsync(ValidatedAuthorizeRequest request)
{
    var result = await base.ProcessLoginAsync(request);
    if (result.IsConsent || result.IsLogin || result.IsError || result.IsRedirect)
        return result;

    var loginObject = GetMyCustomLoginObject(); // cannot access HttpContext here
    if (loginObject.SecurityCodeValidationRequired)
    {
        result = new InteractionResponse
        {
            RedirectUrl = "/account/require2fa"
        };
    } else if (loginObject.AcceptTermsRequired)
    {
        result = new InteractionResponse
        {
            RedirectUrl = "/account/acceptterms"
        };
    } else if (loginObject.IsLocked)
    {
        result = new InteractionResponse
        {
            RedirectUrl = "/account/islocked"
        };
    }

    return result;
}

I am just not sure what to do in that GetMyCustomLoginObject method. Is it possible to inject the HttpContext in this class?

Thanks, Bill

billcunnien commented 1 year ago

I tried adding sessions to IS with the injection of the IHttpContextAccessor in my custom generator per https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-context?view=aspnetcore-7.0. The error is happening on this line:

var loginObject = httpContextAccessor.HttpContext.Session.GetString($"LoginObject{username}");

This is the error I am receiving:

System.InvalidOperationException: Session has not been configured for this application or request. at Microsoft.AspNetCore.Http.DefaultHttpContext.getSession() at Credo.MicroServices.IdentityServer.Generators.CustomAuthorizeInteractionResponseGenerator.ProcessLoginAsync(ValidatedAuthorizeRequest request) in C:\dev\CredoIdentityServer\CredoIdentityServer\Credo.MicroServices.IdentityServer\Generators\CustomAuthorizeInteractionResponseGenerator.cs:line 33 at Duende.IdentityServer.ResponseHandling.AuthorizeInteractionResponseGenerator.ProcessInteractionAsync(ValidatedAuthorizeRequest request, ConsentResponse consent) in //src/IdentityServer/ResponseHandling/Default/AuthorizeInteractionResponseGenerator.cs:line 107 at Duende.IdentityServer.Endpoints.AuthorizeEndpointBase.ProcessAuthorizeRequestAsync(NameValueCollection parameters, ClaimsPrincipal user, Boolean checkConsentResponse) in //src/IdentityServer/Endpoints/AuthorizeEndpointBase.cs:line 120 at Duende.IdentityServer.Endpoints.AuthorizeEndpointBase.ProcessAuthorizeRequestAsync(NameValueCollection parameters, ClaimsPrincipal user, Boolean checkConsentResponse) in //src/IdentityServer/Endpoints/AuthorizeEndpointBase.cs:line 150 at Duende.IdentityServer.Endpoints.AuthorizeCallbackEndpoint.ProcessAsync(HttpContext context) in //src/IdentityServer/Endpoints/AuthorizeCallbackEndpoint.cs:line 50 at Duende.IdentityServer.Hosting.IdentityServerMiddleware.Invoke(HttpContext context, IEndpointRouter router, IUserSession userSession, IEventService events, IIssuerNameService issuerNameService, ISessionCoordinationService sessionCoordinationService) in //src/IdentityServer/Hosting/IdentityServerMiddleware.cs:line 98 at Duende.IdentityServer.Hosting.IdentityServerMiddleware.Invoke(HttpContext context, IEndpointRouter router, IUserSession userSession, IEventService events, IIssuerNameService issuerNameService, ISessionCoordinationService sessionCoordinationService) in //src/IdentityServer/Hosting/IdentityServerMiddleware.cs:line 113 at Duende.IdentityServer.Hosting.MutualTlsEndpointMiddleware.Invoke(HttpContext context, IAuthenticationSchemeProvider schemes) in //src/IdentityServer/Hosting/MutualTlsEndpointMiddleware.cs:line 94 at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at Duende.IdentityServer.Hosting.DynamicProviders.DynamicSchemeAuthenticationMiddleware.Invoke(HttpContext context) in //src/IdentityServer/Hosting/DynamicProviders/DynamicSchemes/DynamicSchemeAuthenticationMiddleware.cs:line 47 at Duende.IdentityServer.Hosting.BaseUrlMiddleware.Invoke(HttpContext context) in //src/IdentityServer/Hosting/BaseUrlMiddleware.cs:line 27 at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Perhaps I did not setup the sessions correctly. I was using this as a guide: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/app-state?view=aspnetcore-7.0#session-state.

Bill

billcunnien commented 1 year ago

I did not setup sessions correctly. First, the registration:

services.AddHttpContextAccessor();

Then, the UseSession has to be added before UseAuthorization:

app.UseSession(); app.UseAuthorization();

Of course, none of this was obviously declared in any of the documentation. I found these clues in StackOverflow comments (not answers, but comments!).

So, my session appears to be running and accessible (at least, no errors), but now, it looks like my validation process is broken. It was working before I started screwing around with the session.

Sigh. I am unable to debug anything either. Is there a trick to doing that? I would like to set a breakpoint within the OnPost of the login index.cshtml.cs. I would like to confirm that the value is actually getting set into the session properly:

HttpContext.Session.SetString($"LoginResponse_{Input.Username}", JsonConvert.SerializeObject(loginResponse));

Later, I would like to deserialize this and use it in the interactions.

brockallen commented 1 year ago

Of course, none of this was obviously declared in any of the documentation.

These are standard ASP.NET features, so we assume you either know how they work already or can refer to the Microsoft docs for how they work.

Getting back to your original question -- you have some result from your login process that requires you to send the user on some custom journey. That's all fine, and the trick is state management using the features of ASP.NET. The only state you must maintain from IdentityServer proper is the original returnUrl passed to your login page. And once your custom workflow is done, you would establish the session using the cookie authentication handler (via the call to SignInAsync) no different than you would on a normal login page, and then redirect to the returnUrl we provided you.

How you convey any state to any other pages in your workflow is up to you using ASP.NET, and we don't require you to use anything specific for that. I think Joe was just mentioning session state as a common approach provided by ASP.NET.

billcunnien commented 1 year ago

So, let's just say that I am plopping an object into the session (and it works). In the CustomAuthorizeInteractionResponseGenerator (which extends AuthorizeInteractionResponseGenerator), I have injected the IHttpContextAccessor into the constructor and overridden the ProcessLoginAsync method. Within that method I am pulling that object out of the session and doing something like this:

if (loginResponse.SecurityCodeValidationRequired)
{
    result = new InteractionResponse
    {
        RedirectUrl = "/account/require2fa"
    };
} else if (loginResponse.AcceptTermsRequired)
{
    result = new InteractionResponse
    {
        RedirectUrl = "/account/acceptterms"
    };
} else if (loginResponse.IsLocked)
{
    result = new InteractionResponse
    {
        RedirectUrl = "/account/islocked"
    };
}

In theory, this should be the way to handle these legs of the custom journey, right?

If there is a different mechanism I should be using within IS, please let me know.

Also, how do I debug this method (or anything else in IS)? Breakpoints simply do not break. I did try adding this to the launchSettings:

"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}"

That seems to have worked in other apps I am working on but not here.

billcunnien commented 1 year ago

Breakpoints working. Big "Duh!" moment on that one. :) Release vs. Debug mode.

I am still not getting sessions to work, so I will go back to the drawing board and figure out what is going on. Will close this for now (assuming the IS path I am taking is correct), since it appears to only be a .NET issue at this point (and my own blindspots).

brockallen commented 1 year ago

Yes, any state you tie to the HTTP "session" (via cookies, etc) from your custom UI should be available on the ~/authorize endpoint in IdentityServer, and thus any services you extend/implement that are invoked on that request (like the authorize interaction response generator) should have access to it.