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

1

9

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.

Mauri answered 28/9, 2012 at 1:37 Comment(0)
A
29

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;
}
Algeciras answered 30/10, 2012 at 13:5 Comment(8)
Excellent! Would it be generally safer to just make the page do a full-reload when the login occurs without a returnUrl?Stichometry
@Stichometry - yes that approach is simpler and would allow you to eliminate the script code for the Ajax call and the associated action method that fulfills the partial page update. But since the parent page has no direct interaction with the server during the auth sequence, the redirect would still need to be triggered, using a script call, by the popup window, just before it closes.Algeciras
Wow, thank you very much Tim for the detailed answer. I'm developing a plugin app that is targeted to be used in multiple domains. I'm wanting to use Facebook authentication as well, but Facebook only supports authentication from a single domain. I think I can take your approach and tweak it so the popup is always going to a single domain that's blessed by Facebook. Then after authentication, I can supply the calling page with authentication results. Thanks again for this well thought out explanation.Mauri
The approach I'm taking has the pop-up in a different domain so that I can use facebook authentication. For a given Facebook app, you can only have it work with a single domain (you can use sub domains etc.). My application will be embeded in numerous client apps (hopefully) so I'm kind of hindered using facebook because of the different domain issue. I can get the pop-up to open following your approach, but it seems to be failing on calling the callback function to send data back to caller page. I can only assume it's a cross domain issue. Thoughts? Thanks.Mauri
@Tom: You're right that Facebook is restrictive, in the sense that it requires the domain to be explicitly configured. I'm not sure if there is a solution to this in your case, but it might help if you could explain how your application is embedded into its client applications.Algeciras
Hi Tim. I'm on my way to figuring this out using your example. I'm embedding my application into clients using jQuery. In essence, I have a DOM element acting as a container for my app. I can pop open a new window for authentication using a URL who's domain is registered with Facebook. In essence the popup acts as a proxy. The LoginResults page closes the popup. I'm using HTML 5's window.postMessage to deliver results back to calling page. I'm working out the kinks now, but it all seems to be working. Thank you very much for your help.Mauri
+1 for the simplified ExternalLoginCallback. I'm only accepting Facebook logins for my app -- no local credentials, twitter, etc -- and so don't need the username confirmation stage including the extra redirects. Thanks!!Tomblin
Excellent!.One thing though. Page it opens is full page for facebook login. How can we open facebook popup page for login. I noticed the url we open has display=page, How can I change it to display=popup ? ThanksEnright

© 2022 - 2024 — McMap. All rights reserved.