Search code examples
asp.net-mvcauthenticationowinowin-middleware

OWIN authentication middleware: logging off


OWIN beginner here. Please be patient...

I'm trying to build an OWIN authentication middleware which uses form posts to communicate with my external authentication provider. I've had some success with getting the authentication bits working. In other words, I can:

  1. communicate with the remote provider through form post;
  2. process the response returned by the remove provider
  3. If everything is ok, I'm able to signal the default authentication provider
  4. THis in turn gets picked up by the cookie middleware which ends up generating the authentication cookie

So far, so good. Now, what I'd like to know is how to handle a log off request. Currently, the controller will simply get the default authentication manager from the owin context and call its SingOut method. This does in fact end my current session (by removing the cookie), but it really does nothing to the existing "external" session.

So, here are my questions: 1. Is the authentication middleware also responsible for performing log off requests? 2. If that is the case, then can someone point me to some docs/examples of how it's done? I've found some links online which describe the logging in part, but haven't found anything about the log off process...

Thanks.

Luis


Solution

  • After some digging, I've managed to get everything working. I'll write a few tips that might help someone with similar problems in the future...

    Regarding the first question, the answer is yes, you can delegate the logoff to the middleware. If you decide to do that, then your middleware handler should override the ApplyResponseGrantAsync method and check if there's a current revoke request. Here's some code that helps to illustrate the principle:

    protected override async Task ApplyResponseGrantAsync() {
        var revoke = Helper.LookupSignOut(Options.AuthenticationType, 
                                                   Options.AuthenticationMode);
        var shouldEndExternalSession = revoke != null;
        if (!shouldEndExternalSession) {
           return;
        }
        //more code here...
     }
    

    After checking if there's a revoke request, and if your external authentication provider is able to end the response through a redirect, then you can simply call the Response.Redirect method (don't forget to check for the existance of redirect - ex.: if you're using asp.net identity and MVC's automatically generated code, then the sign out will redirect you to the home page of your site).

    In my scenario, things were a little more complicated because communication with my authentication provider was based of form posts (SAML2 messages with HTTP Post binding). I've started by trying to use Response.Write to inject the HTML with the autopostback form into the output buffer:

    protected override async Task ApplyResponseGrantAsync() {
        //previous code + setup removed
        var htmlForm = BuildAndSubmitFormWithLogoutData(url, 
                            Options.UrlInicioSessaoAutenticacaoGov);
        Response.StatusCode = 200;
        Response.ContentType = "text/html";
        await Response.WriteAsync(htmlForm);
    }
    

    Unfortunately, it simply didn't work out. Not sure on why, but the browser insisted in redirecting the page to the URL defined by the Logoff's controller method (which was redirecting the page to its home page or '/'). I've even tried to remove the location HTTP header from within the ApplyResponseGrantAsync method, but it still ended up redirecting the user to the home page (instead of loading the predefined HTML I was writing).

    I've ended up changing the redirect so that it gets handled by my middleware. Here's the final code I've ended up with in the ApplyResponseGrant method:

    protected override async Task ApplyResponseGrantAsync() {
        //previous code + setup removed
        //setup urls for callbabk and correlation ids
        var url = ...; //internal cb url that gets handled by this middleware
        Response.Redirect(url);
    }
    

    This redirect forced me to change the InvokeAsync implementation so that it is now responsible for:

    1. Checking for a new authentication session
    2. Checking for the end of an existing authentication session (handle the logoff response from the external provider)
    3. Checking if it should generate a new form post html message that ends the current session controlled by the external provider

    Here's some pseudo code that tries to illustrate this:

    public override async Task<bool> InvokeAsync() {
        if (Options.InternalUrlForNewSession.HasValue && 
                   Options.InternalUrlForNewSession == Request.Path) {
               return await HandleLoginReply(); /login response
        }
        if (Options.InternalUrlExternalSessionEnded.HasValue && 
                   Options.InternalUrlExternalSessionEnded == Request.Path) {
               return await HandleLogoffReply();//logoff response
        }
        if (Options.InternalUrlForEndingSession.HasValue &&   
                    Options.InternalUrlForEndingSession == Request.Path) {
                return await HandleStartLogoutRequest(); //start logoff request 
        }
        return false;
    }
    

    Yes, in the end, I've ended with an extra request, which IMO shouldn't be needed. Again, I might have missed something. If someone manages to get the ApplyResponseGrantAsync to return the auto submit post (instead of the redirect), please let me know how.

    Thanks.