Announcing Microsoft.AspNet.Facebook 1.1 beta

This past June we released the first version of the Microsoft.AspNet.Facebook NuGet package and the corresponding Visual Studio template. Today we released an update to this package: Microsoft.AspNet.Facebook 1.1 beta. In this release, we enabled better support for the Safari browser and added a new feature that gives developers the ability to add custom logic for browsers that do not have cookies enabled. The Visual Studio template that uses this updated package will be released soon. To read more about the features in the 1.0 package, please see the blog post from the initial announcement. The Visual Studio 1.0 template can be found here.

The Microsoft.AspNet.Facebook package tracks permissions requested of the user in a cookie to determine skipped permissions. Prior to this update, skipping optional permissions for ASP.NET Facebook Applications resulted in infinite loops when run in Safari. This was happening because Safari blocked cookies that the Microsoft.AspNet.Facebook package attempted to create. Because cookies were not created, the package did not have a way to determine which permissions were skipped so the permissions dialog would repeat infinitely. Also, there was no error message that could inform the user what was happening. We made this experience much better in this release. Developers can now redirect to an error page when cookies are not enabled.

Features added in this release

CannotCreateCookieRedirectPath Configuration

You can set the CannotCreateCookieRedirectPath in web.config to control where users are redirected when the framework cannot create cookies. If the configuration value is set, the framework redirects the user to the error page; otherwise, it redirects to https://www.facebook.com.

clip_image001[3]

OnCannotCreateCookies Hook

As the name suggests, this hook gets called if the Microsoft.AspNet.Facebook package cannot create cookies due to browser restrictions or the user’s settings. By default, it looks for a redirect path provided by the CannotCreateCookieRedirectPath in FacebookConfiguration.

NOTE: Once this hook gets called NO other prompts are triggered unless the default OnCannotCreateCookies logic is overridden.

Developers can override this hook to add their custom logic if needed.

A few examples are mentioned below.

Set permissionContext.Result to null

Instead of redirecting to an error page or https://www.facebook.com, this takes the user directly to the app without prompting for permissions.

protected override void OnCannotCreateCookies(PermissionContext permissionContext)
{
    permissionContext.Result = null;
}

Redirect to an action

The developer can redirect to an action when this hook gets called.

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

Do NOT set permissionContext.Result to ShowPrompt

Setting permissionContext.Result to ShowPrompt within the OnCannotCreateCookies hook causes infinite prompt loops.

Enabling consistent behavior with cookie enabled browsers

The OnPermissionPrompt and OnCannotCreateCookies methods can be overridden to enable consistent behavior between cookie and non-cookie enabled browsers. In such cases, the requested permissions (that would normally be stored in a cookie) should be persisted to a database. Here is one way to do that.

This example uses EntityFramework to access the database. It creates a class called FacebookPermission to store permissions and a class called FacebookPermissionDbContext for storing permissions in a database.

To get started, open Visual Studio 2013, create a new ASP.NET Facebook application. Then, add the following classes to the project:

public class FacebookPermission
{
    [Key]
    public int FacebookPermissionId { get; set; }
    public string User { get; set; }
    public string RequestedPermissions { get; set; }
}

public class FacebookPermissionDbContext : DbContext
{
    public FacebookPermissionDbContext()
        : base("DefaultConnection")
    {
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
    }

   public virtual DbSet<FacebookPermission> FacebookPermissions { get; set; }
}

Next, create a custom authorization filter that derives from FacebookAuthorizeFilter and override the OnPermissionPrompt and OnCannotCreateCookies methods.

OnPermissionPrompt reads RequestedPermissions from the database and derives skipped permissions. Based on these permissions either OnDeniedPermissionPrompt or OnPermissionPrompt will be invoked.

The OnCannotCreateCookies method persists the requested permissions to the database and calls OnDeniedPermissionPromt on the base class

public class CustomAuthorizeFilter : FacebookAuthorizeFilter
{
    private static FacebookPermissionDbContext db = new FacebookPermissionDbContext();

    public CustomAuthorizeFilter(FacebookConfiguration config)
        : base(config)
    {

    }

    protected override void OnPermissionPrompt(PermissionContext context)
    {
        FacebookPermission storedPermission = db.FacebookPermissions.Where(p => p.User == context.FacebookContext.UserId).FirstOrDefault();
        if (storedPermission == null)
        {
            // This calls into base implementation, you can replace this with your own logic.
            base.OnPermissionPrompt(context);
        }
        else
        {
            IEnumerable<string> requestedPermissions = storedPermission.RequestedPermissions.Split(',');
            IEnumerable<string> declinedPermissions = context.DeclinedPermissions;
            context.SkippedPermissions = requestedPermissions.Except(context.DeclinedPermissions);
            bool deniedPermissions = context.MissingPermissions.Where(
                permission => context.DeclinedPermissions.Contains(permission) ||
                              context.SkippedPermissions.Contains(permission)).Any();

            if (deniedPermissions)
            {
                // This calls into base implementation, you can replace this with your own logic.
                base.OnDeniedPermissionPrompt(context);
            }
            else
            {
                // This calls into base implementation, you can replace this with your own logic.
                base.OnPermissionPrompt(context);
            }

        }
    }

    protected override void OnCannotCreateCookies(PermissionContext context)
    {
        FacebookPermission storedPermission = db.FacebookPermissions.Where(p => p.User == context.FacebookContext.UserId).FirstOrDefault();
        IEnumerable<string> requestedPermissions = context.RequiredPermissions;
        string permissionString = String.Join(",", requestedPermissions);
        if (storedPermission == null)
        {
            db.FacebookPermissions.Add(new FacebookPermission
            {
                User = context.FacebookContext.UserId,
                RequestedPermissions = permissionString
            });
        }
        else
        {
            if (String.IsNullOrEmpty(storedPermission.RequestedPermissions))
            {
                storedPermission.RequestedPermissions = permissionString;
            }
            else
            {
                IEnumerable<string> unPersistedPermissions = requestedPermissions.Except(storedPermission.RequestedPermissions.Split(','));

                foreach (string p in unPersistedPermissions)
                {
                    db.Entry(storedPermission).State = EntityState.Modified;
                    storedPermission.RequestedPermissions = String.Join(",", storedPermission.RequestedPermissions, p);
                }
            }
        }
        db.SaveChanges();

        // This calls into base implementation, you can replace this with your own logic.
        base.OnDeniedPermissionPrompt(context);
    }
}

Open FacebookConfig.cs and replace

GlobalFilters.Filters.Add(new FacebookAuthorizeFilter(configuration));

with

GlobalFilters.Filters.Add(new CustomAuthorizeFilter(configuration));

You can find the complete solution here.

NOTE: This is a guidance sample for simple one page Facebook application. For multi-page Facebook applications, you may have to add additional logic.

Conclusion

Please try this feature and let us know what you think. To find or file issues do so here.