7

We use ADFS for our internal applications - users are basically logged in transparently anytime they go to one of our apps. However, if a user leaves a page open for over an hour then tries to do something on that page (other than navigate to another page), they get an error:

This page is accessing information that is not under its control. This poses a security risk. Do you want to continue?

It seems like the page is trying to redirect that request to the ADFS server and that is being prevented by the browser.

My question is thus: How do I catch this situation and get the user to the ADFS server to reauthenticate?

I haven't had any luck finding anything on Google regarding this.

zimdanen
  • 5,508
  • 7
  • 44
  • 89
  • If it's relevant, hitting "Yes" to continue takes the user to the action - which doesn't exist as a GET, so they then get a 404. – zimdanen Mar 27 '13 at 20:28
  • Did you find any neat solution? In our case, we're contemplating (if we can detect the issue happening) adding a new (hidden) `iframe` onto the page that accesses a new page in the relevant internal app. Loading that new page into the `iframe` should trigger off a full passive flow against the ADFS server that eventually will load the new page after re-authenticating. At that point, the new page will notify the existing page that the cycle is complete via `postMessage`, and the appropriate cookies should be available again. – Damien_The_Unbeliever Aug 29 '13 at 12:30
  • 1
    @Damien_The_Unbeliever: We haven't continued to dig into this due to time issues and other priorities. The iframe solution feels hacky to me, but, if it's the only way to fix the issue, it is what it is. Let me know how it works out for you. – zimdanen Aug 29 '13 at 13:02
  • @Damien_The_Unbeliever: Were you guys able to get this working? – zimdanen Oct 04 '13 at 21:02
  • We permanently have a hidden iframe going to a blank page on the app with a meta `refresh` tag set lower than the session timeout. It seems to be keeping everything working, at the expense of a) slight ugliness and b) an extra round-trip every 15 minutes or so. – Damien_The_Unbeliever Oct 05 '13 at 05:26

2 Answers2

3

Update: The below solution depends on iframes. ADFS 3.0 has X-Frame-Options defaulted to DENY, with no option to change the setting. So this solution will only work on ADFS 2.1 & earlier.

In your global.asax.cs, you're going to want to catch any mid-AJAX 302s and turn them into a 401 Unauthorized. This will prevent the call from proceeding (and popping that message), and will send us to $(document).ajaxError().

    protected void Application_EndRequest()
    {
        var context = new HttpContextWrapper(this.Context);
        if (context.Response.StatusCode == 302 && context.Request.IsAjaxRequest())
        {
            context.Response.Clear();
            context.Response.StatusCode = 401;
        }
    }

Then, in there, intercept any 401s before they proceed to the rest of your error handling. I chose to show a message to the users. You can do the next step right here, but for readability, I'm sending the ajaxSettings object to another function. Return true so it won't proceed into the rest of your error handling.

If you want to doublecheck that this is ADFS, event.target.referrer will have the URL of the attempted redirect.

$(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) {
    if (xhr.status == 401) { 
        alert("Your session has timed out. Click OK to reauthorize and extend your session.");

        TriggerReauthenticationRefresher(ajaxSettings); 
        return true;
    }
…the rest of the error handling code…            
});

I have an empty div in my page just for this situation, with an id of 'refresherBox', but you can do this on any element in your DOM. Put together an iframe that goes to some dummy page in your domain. In my case, the contents of ADFSRefresher.cshtml are just

 <div><input type="hidden" value="@DateTime.Now.ToString()" /></div>

Instead of using global variables, I'm storing the ajaxSettings using .data(). We also need to keep track of how many times the iframe reloads, so we're also storing loadcount. Insert the iframe into the DOM, and it will kick off.

function TriggerReauthenticationRefresher(ajaxSettings) {
    var refreshframe = '<iframe src="@Url.Action("ADFSRefresher", "Debug")" style="display:none" onload="TrackFrameReloads()" />';

    $('#refresherBox').data('loadcount', 0);
    $('#refresherBox').data('originalRequestSettings', ajaxSettings);

    $('#refresherBox').html(refreshframe);
}

TrackFrameReloads will fire every time the iframe finishes loading. Since we know there is an impending ADFS redirect, it will fire twice. The first time will be the redirect, and the second time will be to its src url. So the first time it fires, we just increment loadcount.

The second time it fires, we know we have been successfully reauthenticated. Retrieve the ajaxSettings, clear the stored data, and you can then re-use your original settings to send the AJAX call! It will go through, un-redirected, and run its original success & complete functions.

function TrackFrameReloads() {
    var i = $('#refresherBox').data('loadcount');
    if (i == 1) {
        alert('Your session has been extended.');

        var ajaxSettings = $('#refresherBox').data('originalRequestSettings');

        $('#refresherBox').removeData();

        $.ajax(ajaxSettings);

    } else {
        $('#refresherBox').data("loadcount", 1);
    }
}

Be aware that if you defined them, the error and complete functions will have already fired.

You can skip the two alert messages to the users if you like. Depending on your ADFS setup, this should only take 1 second, and the user doesn't have to be informed that any of this happened at all!

friggle
  • 3,362
  • 3
  • 35
  • 47
  • This works but I feel like it would be better to know earlier in the request that the session is expired and do something then rather than cannibalizing all of your 302 responses. – MetaGuru Jan 31 '14 at 16:17
  • ADFS won't allow me to load in an iFrame either: Refused to display in a frame because it set 'X-Frame-Options' to 'DENY'. Any way around this? – severin Feb 05 '15 at 13:29
  • 1
    Do you know which version of ADFS it is? Apparently ADFS 3 defaults the setting to DENY, and offers no way to change it. I'll update my answer to note that this solution only works with ADFS 2 and below. I actually have moved away from using the posted solution, and instead have begun using sliding sessions. This post was extremely helpful for learning about this: http://www.cloudidentity.com/blog/2010/06/16/warning-sliding-sessions-are-closer-than-they-appear/ – friggle Feb 05 '15 at 20:06
1

You can inspect and re-issue security tokens manually in global.asax, and use this to create sliding sessions. With sliding sessions, you can choose to postpone the re-authentication until it becomes "safe" to do so (when data will no longer be lost due to the ADFS redirect).

Inside the SessionSecurityTokenReceived event, you can evaluate the token and the request. If the token has expired and the request is one that will experience data loss from a redirect, you can re-issue a new "temporary" token. The new token should have a very short life, just long enough so you can safely complete the current request. The token will then expire and be evaluated again on the next request.

protected void SessionAuthenticationModule_SessionSecurityTokenReceived(object sender, SessionSecurityTokenReceivedEventArgs e)
{
    var now = DateTime.UtcNow;
    SessionSecurityToken token = e.SessionToken;
    var httpContext = new HttpContextWrapper(this.Context);

   if (now > token.ValidTo
       && (httpContext.Request.IsAjaxRequest() || httpContext.Request.HttpMethod == "POST"))
   {
       var sessionAuthModule = (SessionAuthenticationModule)sender;
       e.SessionToken = sessionAuthModule.CreateSessionSecurityToken(token.ClaimsPrincipal,
                                                                     token.Context,
                                                                     now,
                                                                     now.AddMinutes(2),
                                                                     token.IsPersistent);
       e.ReissueCookie = true;
   }
}

The ADFS session will continue to postpone reauthentication until the next GET request. Then the redirect will finally occur, and the user will be issued a proper token of normal lifespan.

friggle
  • 3,362
  • 3
  • 35
  • 47
  • I'm in the same scenario of the initial question, and I'm trying this solution. Which nuget package I have to install? This handler is never fired – andrea.spot. Jan 20 '17 at 10:47
  • This makes use of the Microsoft Token Validation Extension package (System.IdentityModel.Tokens.ValidatingIssuerNameRegistry) – friggle Jan 20 '17 at 19:49