Updating the MVC Facebook API

Updating the MVC Facebook API

  • Comments 10

Over the past several months Facebook made changes to their application development APIs that were incompatible with the MVC Facebook support.

We have been working on updates while the Facebook API kept evolving, and on 4/30/2014 Facebook announced a two-year stability guarantee. This was a fantastic announcement because this ensured a similar stability for ASP.NET MVC developers developing Facebook applications. We've fixed the Facebook package and renamed it to Microsoft.AspNet.Facebook. This package is now available on NuGet.

Here are the issues that we’ve fixed.

If you’re new to the world of Facebook application development on MVC you can check out a Birthday application here. Keep in mind this tutorial will be gradually updated so it may not be entirely accurate.

The important new stuff

The original Microsoft.AspNet.Mvc.Facebook package and corresponding API’s at the time had no concept of optional or default permissions. This created friction with some of the updated prompt dialogs that Facebook released. To address this we’ve made the Facebook’s authorize filter more flexible by providing permission prompt hooks to control login dialog flow.

FacebookAuthorizeFIlter now exposes OnPermissionPrompt and an OnDeniedPermissionPrompt hooks. So what do these hooks do? Let’s go over each in detail.

OnPermissionPrompt

Every time a prompt is about to be shown the OnPermissionPrompt is invoked and passed a PermissionContext. The hook enables you to modify the login flow by setting the context's Result property just like you’d do in an authorization filter.

To utilize the OnPermissionPrompt you can create a FacebookAuthorizeFilter like so:

public class CustomFacebookAuthorizeFilter : FacebookAuthorizeFilter
{
    public CustomFacebookAuthorizeFilter(FacebookConfiguration config)
        : base(config)
    { }

    protected override void OnPermissionPrompt(PermissionContext context)
    {
        // This sets context.Result to ShowPrompt(context) (default behavior)
        base.OnPermissionPrompt(context);
    }
}


And then you can apply the filter the same way you’d apply a FacebookAuthorizeFilter in the old world:

GlobalFilters.Filters.Add(new CustomFacebookAuthorizeFilter(yourFacebookConfiguration));

The default behavior of the OnPermissionPrompt hook is to set the PermissionContext's result to a ShowPromptActionResult by calling into the ShowPrompt method. The source code of the OnPermissionPrompt method looks like this:

protected virtual void OnPermissionPrompt(PermissionContext context)
{
    context.Result = ShowPrompt(context);
}

The ShowPrompt method returns an action result that shows the permission prompt to the user. We can do more though; if we really wanted to, we could redirect the user to a different action every time a prompt was about to be shown by overriding the default signature as shown:

protected override void OnPermissionPrompt(PermissionContext context)
{
    context.Result = new RedirectToRouteResult(
        new RouteValueDictionary
        {
            { "controller", "home" },
            { "action", "foo" }
        });
}

This would redirect us to the Home controller’s action Foo instead of prompting the user for permissions.

Lastly we could modify the OnPermissionPrompt method to ignore every prompt that’s shown via setting the PermissionContext's Result to be null:

protected override void OnPermissionPrompt(PermissionContext context)
{
    context.Result = null;
}

This would make it so we never prompt a user for permissions. This isn’t ideal but it is possible.

OnDeniedPermissionPrompt

The OnDeniedPermissionPrompt is invoked when we detect that a user has revoked, declined, or skipped permissions. This occurs instead of the OnPermissionPrompt hook when there are denied permissions. Just like the OnPermissionPrompt we can utilize this hook by creating a FacebookAuthorizeFilter like so:

public class CustomFacebookAuthorizeFilter : FacebookAuthorizeFilter
{
    public CustomFacebookAuthorizeFilter(FacebookConfiguration config)
        : base(config)
    { }

    protected override void OnDeniedPermissionPrompt(PermissionContext context)
    {
        // Does nothing (default behavior to leave context.Result null)
        base.OnDeniedPermissionPrompt(context);
    }
}

And then just like the OnPermissionPrompt above you can apply the filter the same way you’d apply a FacebokAuthorizeFilter in the old world:

GlobalFilters.Filters.Add(new CustomFacebookAuthorizeFilter(yourFacebookConfiguration));

The default behavior of the OnDeniedPermissionPrompt hook is to leave the passed in PermissionContext’s Result member null to ignore the “denied” permission prompt. The source code of the OnDeniedPermissionPrompt method looks like this:

protected virtual void OnDeniedPermissionPrompt(PermissionContext context)
{
}

Here we’re doing nothing so the PermissionContext's Result member is null; this indicates that we do not want to show the permission prompt to the user. If we were to do the same thing as OnPermissionPrompt and set the PermissionContext's Result to be a ShowPromptActionResult we’d infinite loop in our login dialogs; the reason why is because every time the method is invoked there would still be denied permissions.

Like the OnPermissionPrompt hook you can modify the login flow with the result that you return. For example, let’s say we wanted to redirect to a “skip” handling page if we detect a user has skipped permissions:

protected override void OnDeniedPermissionPrompt(PermissionContext context)
{
    if (context.SkippedPermissions.Any())
    {
        context.Result = new RedirectResult("http://www.contoso.com/SomeUrlToHandleSkips");
    }
}

PermissionContext

In the previous sections I did not discuss the PermissionContext object that is passed into both the OnPermissionPrompt and OnDeniedPermissionPrompt hooks. This object exposes a significant amount of information that enable developers to modify the login flow. Let’s examine what the PermissionContext has to offer:

  • IEnumerable<string> DeclinedPermissions
    • Permissions that were previously requested for but not granted for the lifetime of the application. This can happen by a user revoking, skipping or choosing not to allow permissions in the Facebook login dialog.
  • FacebookContext FacebookContext
    • Provides access to Facebook-specific information.
  • AuthorizationContext FilterContext
    • Provides access to filter information.
  • IEnumerable<string> MissingPermissions
    • The entire list of missing permissions for the current page, including DeclinedPermissions and SkippedPermissions.
  • HashSet<string> RequiredPermissions
    • The entire list of requested permissions for the current page. Includes permissions that were already prompted for.
  • IEnumerable<string> SkippedPermissions
    • Permissions that were previously requested for but skipped. This can happen from a user hitting the "skip" button when requesting permissions.
  • ActionResult Result
    • The ActionResult that should be used to control the login flow. If value is null then we will continue onto the action that is intended to be invoked. Non-null values short-circuit the action.

With all of the information the PermissionContext has to offer you should be able to fully control the login flow.

Hook flow chart

For reference sake I’ve created a flow chart to indicate how Facebook’s login dialog and our hooks interact.

clip_image001

Upgrading Existing Facebook Applications

To upgrade an existing Facebook application follow these steps:

  1. If your application does not currently run; migrate to the latest Facebook application platform.
  2. In the package manager console run: Uninstall-Package Microsoft.AspNet.Mvc.Facebook
  3. Rename all Microsoft.AspNet.Mvc.Facebook namespaces to Microsoft.AspNet.Facebook.
  4. In the package manager console run: Install-Package Microsoft.AspNet.Facebook

Conclusion

We’d love to see people try out the new Microsoft.AspNet.Facebook package and let us know what you think. If you have any questions feel free to reach out below.

To find or file issues do so here.

Note: Facebook changed the way their “user_friends” permission works.  It used to return all of a users friends and now only returns friends that also have your application.  This will obviously be a limiter when using the existing Facebook template and the Birthday App tutorial.

Leave a Comment
  • Please add 5 and 3 and type the answer here:
  • Post
  • This is an interesting change. How does this effect the current AccountController ? Currently this controller manages authentication for other OAUTH providers. Is this a replacement ? Say, a facebook only website ?

  • This does not change the AccountController. This change is to support developers building facebook canvas apps. Please look at the following tutorial for reference www.asp.net/.../aspnet-mvc-facebook-birthday-app

  • NICE INFORMATION...

  • nice

  • This brought me close in making a page tab app work with the library, but still, when the user navigates I am losing the context and need to re-redirect, which loses my form variables then.

    Is this library by intention not made for being compatible with page tab apps?

    Best regards,

    Florian

  • This would redirect us to the Home controller’s action Foo instead of prompting the user for permissions..

    thank you

    webelio.com/%D8%B3%D8%A6%D9%88

  • Thanks for all the great info.. I'm still coming up to speed with a few things.

    I am starting with trying  to handle my FB Page realtime events.

    I have something strange going on where the HandleUpdateAsync never fires.. but If I override the "POST" , this does get hit on the updates to the page.

    I am pretty sure the POST contains the update.. as I can see the the Content_length change between posts and a reply. ( which looks about right)

    + Request {Method: POST, RequestUri: 'www.xxxx.co.nz/.../UserRealtimeUpdate&,

    Version: 1.1, Content: System.Net.Http.StreamContent, Headers:

    {    Connection: keep-alive    Accept: */*    Host: www.XXXX.co.nz  

    X-Hub-Signature: sha1=f9563ec5bcb650e6cdc4d7cf2ecec2c7b696de52    Content-Length: 206    Content-Type: application/json  }}

    System.Net.Http.HttpRequestMessage

    - Request {Method: POST, RequestUri: 'www.xxxx.co.nz/.../UserRealtimeUpdate&,

    Version: 1.1, Content: System.Net.Http.StreamContent, Headers:

    {    Connection: keep-alive    Accept: */*   Host: www.XXXX.co.nz

    X-Hub-Signature: sha1=a704fe343807e9299de5cfd5c983d0f8852b505a    Content-Length: 286

    Content-Type: application/json  }} System.Net.Http.HttpRequestMessage

    I have the latest version.. ( I believe).

    If I change the URL for the events to another php server its working fine.. likewise the subscribe is working fine as well.

    Any ideas anyone?

    Thanks!

    Rob

  • Just an update.

    Managed to create my own Event Object from Result ( Result.content.ReadAsStringAsync().Result ) and parsing to a strongly typed Class that works with Page Feed updates.

    So I have everything working using the overridden POST function.

    I'm guessing something is going wrong with Page feed updates ( page feed have allot more detail than others) and they are failing to be converted to a "ChangeEntry" and so the HandleUpdateAsync  never occurs.

    I am using the latest 'stable' release.. I cant install the pre release.. fails for me.., so maybe this has been fixed already :)

    Cheers

    Rob

  • I think I'm talking to myself.. but just in case someone is reading this!

    I think my theory was wrong , because testing with another page with the events coming to the same controller, I get a HandleUpdateAsync  with type 'page' and no changes. ( I guess that not implemented yet)

    The only thing that looks like it could be the issue is the id of the object. The working page has an id that is 15 characters.. where the page that never fires the HandleUpdateAsync is 17 characters.

    Regards

    Rob

  • @Rob den Boer - Can you please provide a simple repro for the issue you mentioned?

    Thanks,

    Kirthi

Page 1 of 1 (10 items)