Can ASP.Net MVC 4's OAuthWebSecurity open a pop-up

Tom Schreck picture Tom Schreck · Sep 28, 2012 · Viewed 7.1k times · Source

I'm trying to figure out how to use ASP.Net MVC 4's new OAuthWebSecurity functionality. Is it possible when clicking on the facebook or twitter external login button to have the form post to a pop-up instead of refreshing the current page? I've used oauth with Twitter and Facebook before using Javascript and the external authentication would happen in a pop-up. After the results is returned asynchronously, the popup would close. Can i do something similar to this using MVC 4's new OAuthWebSecurity functionality? Thanks.

Answer

Tim Coulter picture Tim Coulter · Oct 30, 2012

There are several aspects to solving this problem:

  1. Opening a popup to accommodate the authentication sequence.
  2. Closing the popup when the authentication is complete.
  3. Handling authentication failure.
  4. Updating the parent page to reflect the fact that the user is authenticated.

Here's how I implemented these requirements, using the MVC4 Internet Application template as a starting point:

To launch the authentication sequence in a popup (instead of redirecting to a new page) you need to modify _ExternalLoginListPartial.cshtml so that its form postback is targeted to a popup window that is launched by a JavaScript function:

@model ICollection<AuthenticationClientData>

@if (Model.Count == 0)
{
    <div class="message-info">
        <p>There are no external authentication services configured. See <a href="http://go.microsoft.com/fwlink/?LinkId=252166">this article</a>
        for details on setting up this ASP.NET application to support logging in via external services.</p>
    </div>
}
else
{
    <form id="login-launch" action="@Url.Action("ExternalLogin", "Account")" method="POST" target="login-popup" onsubmit="invokeLogin();">
        @Html.AntiForgeryToken()
        <fieldset id="socialLoginList">
            <input type="hidden" id="provider" name="provider" />
            <input type="hidden" name="returnUrl" value="@ViewBag.ReturnUrl"/>
            <p>
                @foreach (var p in OAuthWebSecurity.RegisteredClientData)
                {
                    <button type="submit" onclick="$('#provider').attr('value', '@p.DisplayName'); $('#login-launch').submit();" title="Log in using @p.DisplayName">@p.DisplayName</button>
                }
            </p>
        </fieldset>
    </form>
}

<script type="text/javascript">
    function invokeLogin() {
        var chrome = 100;
        var width = 500;
        var height = 500;
        var left = (screen.width - width) / 2;
        var top = (screen.height - height - chrome) / 2;
        var options = "status=0,toolbar=0,location=1,resizable=1,scrollbars=1,left=" + left + ",top=" + top + ",width=" + width + ",height=" + height;
        window.open("about:blank", "login-popup", options);
    }
</script>

In its present state, this code correctly launches the popup and allows the authentication sequence to execute, but the popup remains open and, if a redirect URL was specified, the popup displays this page instead of redirecting the parent page to this URL.

To get the popup to close itself after a successful (or failed) authentication entails modifying the controller action method that handles the authentication callback, such that it returns a custom view containing JavaScript that dismisses the popup. As we'll see below, this mechanism can also be used to implement solutions to goals [3] and [4] above.

[AllowAnonymous]
public ActionResult ExternalLoginCallback(string returnUrl)
{
    var result = OAuthWebSecurity.VerifyAuthentication(Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl }));

    if (!result.IsSuccessful)
    {
        return View("LoginResult", new LoginResultViewModel(false, Url.Action("ExternalLoginFailure")));
    }

    if (OAuthWebSecurity.Login(result.Provider, result.ProviderUserId, createPersistentCookie: false))
    {
        return View("LoginResult", new LoginResultViewModel(true, returnUrl));
    }

    OAuthWebSecurity.CreateOrUpdateAccount(result.Provider, result.ProviderUserId, result.UserName);
    return View("LoginResult", new LoginResultViewModel(true, returnUrl));
}

This action method is a simplified version of the ExternalLoginCallback() method that comes with the original project template. Unlike the original implementation, this simplified sample does not support allowing the user to define a personalized user name when a creating a new account, nor does it allow multiple OAuth or OpenID accounts to be associated with a single MVC user account. However, these capabilities are feasible by extending the above pattern to incorporate the more complex logic of the original template.

A key design feature of the above action method is that it always returns the same view, regardless of the outcome of the authentication attempt. This is necessary, because the returned view contains the JavaScript that closes the authentication popup and invokes any required consequential actions in the parent page. Therefore, if you modify the above pattern, you must ensure that every code path returns an instance of the LoginResult view, correctly populated according to the outcome of the authentication.

Here is the markup for the Loginresult view:

@model LoginResultViewModel
@{
    Layout = null;

    var success = Model.Success ? "true" : "false";
    var returnUrl = Model.ReturnUrl == null ? "null" : string.Format("'{0}'", Model.ReturnUrl);
}

<!DOCTYPE html>
<html>
<head>
    <script type="text/javascript">
        if (window.opener && window.opener.loginCallback) {
            window.opener.loginCallback(@success, @Html.Raw(returnUrl));
        }

        window.close();
    </script>
</head>
</html>

The above view accepts a model of type LoginResultViewModel that reflects the outcome of the completed authentication attempt:

public class LoginResultViewModel
{
    public LoginResultViewModel(bool success, string returnUrl)
    {
        Success = success;
        ReturnUrl = returnUrl;
    }

    public bool Success { get; set; }
    public string ReturnUrl { get; set; }
}

With all of the above elements in place, it is possible to launch an authentication sequence that executes in a popup window that automatically closes itself when the sequence completes. If authentication was successful, the user will be logged-in at this point and if it was launched with a return URL (as would occur automatically if triggered by a request to an action method protected by an [Authorize] attribute), the parent page will be redirected to the originally requested URL.

However, if authentication was launched explicitly by the user (for example, by visiting the login page) the parent page will not redirect and may therefore require a partial-page update to reflect the fact that the user is now logged-in. In the MVC template sample, it is necessary to update the page to show the name of the user and a Logout button instead of the Login and Register buttons.

This can be accomplished by defining a JavaScript callback function in the layout view that is called by the JavaScript executed by the authentication popup:

<script type="text/javascript">
    function loginCallback(success, returnUrl) {
        if (returnUrl) {
            window.location.href = returnUrl;
        } else {
            $.ajax({
                url: '@Url.Action("LoginPartial", "Account")',
                success: function (result) {
                    $('#login').html(result);
                }
            });
        }
    }
</script>

The above JavaScript makes an AJAX call to a new action method that renders and returns the existing _LoginPartial view:

[HttpGet]
public ActionResult LoginPartial()
{
    if (Request.IsAjaxRequest())
    {
        return View("_LoginPartial");
    }

    return new EmptyResult();
}

One final modification is required to the original project template. The _LoginPartial view must be modified to render without a layout view:

@{
    Layout = null;
}