Skip to content

Commit

Permalink
Facebook: Add cannot create cookie error page.
Browse files Browse the repository at this point in the history
- Added a cannot create error page for circumstances that prevent us from creating cookies for users.
- We only re-direct to this error page if we encounter a situation where we may need cookies to function properly.
- Added an authorize filter hook to control when the redirection occurs.
- Added a configuration value to make the no cookies error page act similarly to the existing error page.
  • Loading branch information
NTaylorMullen committed Aug 27, 2014
1 parent d9f8a53 commit 8c87afb
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ namespace Microsoft.AspNet.Facebook.Authorization
/// </summary>
public class FacebookAuthorizeFilter : IAuthorizationFilter
{
private static readonly Uri DefaultAuthorizationRedirectUrl = new Uri("https://www.facebook.com/");
private const string ExecuteMethodCannotBeCalledFormat = "The {0} execute method should not be called.";
private static readonly Uri FacebookUri = new Uri("https://www.facebook.com/");
private static readonly Uri DefaultAuthorizationRedirectUrl = FacebookUri;
private static readonly Uri DefaultCannotCreateCookiesRedirectPath = FacebookUri;
// Facebook Missing Permissions, shortened version to not add excessively long query string parameters to the url.
private const string MissingPermissionsQueryName = "__fb_mps";

private FacebookConfiguration _config;

Expand Down Expand Up @@ -77,6 +80,7 @@ public virtual void OnAuthorization(AuthorizationContext filterContext)

NameValueCollection parsedQueries = HttpUtility.ParseQueryString(request.Url.Query);
HashSet<string> requiredPermissions = PermissionHelper.GetRequiredPermissions(authorizeAttributes);

bool handleError = !String.IsNullOrEmpty(parsedQueries["error"]);

// This must occur AFTER the handleError calculation because it modifies the parsed queries.
Expand Down Expand Up @@ -141,11 +145,29 @@ public virtual void OnAuthorization(AuthorizationContext filterContext)
missingPermissions,
permissionContext.DeclinedPermissions);

// Add a query string parameter that enables us to identify if we've already prompted for missing permissions
// and therefore can detect cookies.
AddCookieVerificationQuery(parsedQueries);
// Rebuild the redirect Url so it contains the new query string parameter.
redirectUrl = GetRedirectUrl(request, parsedQueries);

PromptMissingPermissions(permissionContext, redirectUrl);
}
}
}

/// <summary>
/// Adds a query string parameter to a <see cref="NameValueCollection"/> that enables a
/// <see cref="FacebookAuthorizeFilter"/> to detect when cookies are unavailable and then trigger the
/// <see cref="OnCannotCreateCookies"/> hook.
/// </summary>
/// <param name="queries">List of query parameters that are used to create a url.</param>
protected static void AddCookieVerificationQuery(NameValueCollection queries)
{
// Add a query string parameter that enables us to identify if we've already prompted for missing permissions
queries.Add(MissingPermissionsQueryName, String.Empty);
}

/// <summary>
/// Called when authorization fails and need to create a redirect result.
/// </summary>
Expand Down Expand Up @@ -176,6 +198,27 @@ protected ShowPromptResult ShowPrompt(PermissionContext context)
return new ShowPromptResult(navigationUrl);
}

/// <summary>
/// Invoked during <see cref="OnAuthorization"/> after determining that cookies cannot be created. Default behavior
/// redirects to a no cookies error page.
/// </summary>
/// <param name="context">Provides access to permission information associated with the user.</param>
protected virtual void OnCannotCreateCookies(PermissionContext context)
{
Uri redirectPath;

if (String.IsNullOrEmpty(_config.CannotCreateCookieRedirectPath))
{
redirectPath = DefaultCannotCreateCookiesRedirectPath;
}
else
{
redirectPath = GetCannotCreateCookiesUri();
}

context.Result = CreateRedirectResult(redirectPath);
}

/// <summary>
/// Invoked during <see cref="OnAuthorization"/> when a prompt requests permissions that were skipped or revoked.
/// Set the <paramref name="context"/>'s Result property to modify login flow.
Expand Down Expand Up @@ -213,6 +256,14 @@ private Uri GetErroredAuthorizeUri(string originUrl, HashSet<string> requiredPer
return authorizationUrlBuilder.Uri;
}

private Uri GetCannotCreateCookiesUri()
{
UriBuilder noCookiesUrlBuilder = new UriBuilder(new Uri(_config.AppUrl));
noCookiesUrlBuilder.Path += _config.CannotCreateCookieRedirectPath.Substring(1);

return noCookiesUrlBuilder.Uri;
}

private string GetRedirectUrl(HttpRequestBase request)
{
NameValueCollection queryNameValuePair = HttpUtility.ParseQueryString(request.Url.Query);
Expand Down Expand Up @@ -273,21 +324,32 @@ private void PromptMissingPermissions(PermissionContext permissionContext, strin

permissionContext.RedirectUrl = redirectUrl;

// The DeniedPermissionPromptHook will only be invoked if we detect there are denied permissions.
// It is attempted instead of the permission hook to allow app creators to handle situations when a user
// skip's or revokes previously prompted permissions. Ex: redirect to a different page.
if (deniedPermissions)
// See if our persisted permissions cookie doesn't exist and if we've had missing permissions before.
// Essentially this checks to see if we've tried to persist permissions before and were unsuccessful due to an
// inability to create cookies.
if (!PermissionHelper.RequestedPermissionsCookieExists(filterContext.HttpContext.Request) &&
filterContext.HttpContext.Request.QueryString.Get(MissingPermissionsQueryName) != null)
{
OnDeniedPermissionPrompt(permissionContext);
OnCannotCreateCookies(permissionContext);
}
else
{
OnPermissionPrompt(permissionContext);
}
// The DeniedPermissionPromptHook will only be invoked if we detect there are denied permissions.
// It is attempted instead of the permission hook to allow app creators to handle situations when a user
// skip's or revokes previously prompted permissions. Ex: redirect to a different page.
if (deniedPermissions)
{
OnDeniedPermissionPrompt(permissionContext);
}
else
{
OnPermissionPrompt(permissionContext);
}

// We persist the requested permissions in a cookie to know if a permission was denied in any way.
// The persisted data allows us to detect skipping of permissions.
PermissionHelper.PersistRequestedPermissions(filterContext, requiredPermissions);
// We persist the requested permissions in a cookie to know if a permission was denied in any way.
// The persisted data allows us to detect skipping of permissions.
PermissionHelper.PersistRequestedPermissions(filterContext, requiredPermissions);
}

filterContext.Result = permissionContext.Result;
}
Expand Down
1 change: 1 addition & 0 deletions src/Microsoft.AspNet.Facebook/FacebookAppSettingKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ internal static class FacebookAppSettingKeys
public static readonly string AppNamespace = "Facebook:AppNamespace";
public static readonly string AppUrl = "Facebook:AppUrl";
public static readonly string AuthorizationRedirectPath = "Facebook:AuthorizationRedirectPath";
public static readonly string CannotCreateCookiesRedirectPath = "Facebook:CannotCreateCookiesRedirectPath";
}
}
46 changes: 40 additions & 6 deletions src/Microsoft.AspNet.Facebook/FacebookConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class FacebookConfiguration
private readonly ConcurrentDictionary<object, object> _properties = new ConcurrentDictionary<object, object>();
private string _appUrl;
private string _authorizationRedirectPath;
private string _cannotCreateCookieRedirectPath;

/// <summary>
/// Gets or sets the App ID.
Expand All @@ -35,7 +36,9 @@ public class FacebookConfiguration
public string AppNamespace { get; set; }

/// <summary>
/// Gets or sets the URL path that the <see cref="Microsoft.AspNet.Facebook.Authorization.FacebookAuthorizeFilter"/> will redirect to when the user did not grant the required permissions.
/// Gets or sets the URL path that the <see cref="Microsoft.AspNet.Facebook.Authorization.FacebookAuthorizeFilter"/> will
/// redirect to when the user did not grant the required permissions. If value is not set it will result in a redirection
/// to Facebook's home page.
/// </summary>
public string AuthorizationRedirectPath
{
Expand All @@ -45,15 +48,30 @@ public string AuthorizationRedirectPath
}
set
{
// Check for '~/' prefix while allowing null or empty value to be set.
if (!String.IsNullOrEmpty(value) && !value.StartsWith("~/", StringComparison.Ordinal))
{
throw new ArgumentException(Resources.InvalidAuthorizationRedirectPath, "value");
}
EnsureRedirectPath(value, "AuthorizationRedirectPath");
_authorizationRedirectPath = value;
}
}

/// <summary>
/// Gets or sets the URL path that the <see cref="Microsoft.AspNet.Facebook.Authorization.FacebookAuthorizeFilter"/> will
/// redirect to when the we determine that we are unable to create cookies. If value is not set it will result in a
/// redirection to Facebook's home page.
/// </summary>
public string CannotCreateCookieRedirectPath
{
get
{
return _cannotCreateCookieRedirectPath;
}
set
{
EnsureRedirectPath(value, "CannotCreateCookieRedirectPath");

_cannotCreateCookieRedirectPath = value;
}
}

/// <summary>
/// Gets or sets the absolute URL for the Facebook App.
/// </summary>
Expand Down Expand Up @@ -126,6 +144,22 @@ public virtual void LoadFromAppSettings()
AppNamespace = ConfigurationManager.AppSettings[FacebookAppSettingKeys.AppNamespace];
AppUrl = ConfigurationManager.AppSettings[FacebookAppSettingKeys.AppUrl];
AuthorizationRedirectPath = ConfigurationManager.AppSettings[FacebookAppSettingKeys.AuthorizationRedirectPath];
CannotCreateCookieRedirectPath =
ConfigurationManager.AppSettings[FacebookAppSettingKeys.CannotCreateCookiesRedirectPath];
}

private static void EnsureRedirectPath(string value, string redirectParameterName)
{
// Check for '~/' prefix while allowing null or empty value to be set.
if (!String.IsNullOrEmpty(value) && !value.StartsWith("~/", StringComparison.Ordinal))
{
throw new ArgumentException(
String.Format(
CultureInfo.CurrentCulture,
Resources.InvalidRedirectPath,
redirectParameterName),
"value");
}
}

private string GetAppUrl()
Expand Down
6 changes: 5 additions & 1 deletion src/Microsoft.AspNet.Facebook/JavaScriptRedirectResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,21 @@ namespace Microsoft.AspNet.Facebook
/// </summary>
public class JavaScriptRedirectResult : ContentResult
{
internal Uri RedirectUrl { get; private set; }

/// <summary>
/// Creates a JavaScript based redirect <see cref="ActionResult"/>.
/// </summary>
/// <param name="redirectUrl">The url to redirect to.</param>
public JavaScriptRedirectResult(Uri redirectUrl)
{
RedirectUrl = redirectUrl;

ContentType = "text/html";
Content = String.Format(
CultureInfo.InvariantCulture,
"<script>window.top.location = '{0}';</script>",
HttpUtility.JavaScriptStringEncode(redirectUrl.AbsoluteUri));
HttpUtility.JavaScriptStringEncode(RedirectUrl.AbsoluteUri));
}
}
}
5 changes: 5 additions & 0 deletions src/Microsoft.AspNet.Facebook/PermissionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ public static void PersistRequestedPermissions(AuthorizationContext context, IEn
responseCookies.Add(cookie);
}

public static bool RequestedPermissionsCookieExists(HttpRequestBase request)
{
return request.Cookies.Get(RequestedPermissionCookieName) != null;
}

private static IEnumerable<string> GetPermissionsWithStatus(PermissionsStatus permissionsStatus,
PermissionStatus status)
{
Expand Down
8 changes: 4 additions & 4 deletions src/Microsoft.AspNet.Facebook/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/Microsoft.AspNet.Facebook/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@
<data name="CircularReferenceNotSupported" xml:space="preserve">
<value>Circular type references are not supported.</value>
</data>
<data name="InvalidAuthorizationRedirectPath" xml:space="preserve">
<value>Invalid AuthorizationRedirectPath. The AuthorizationRedirectPath can only be set relative to the AppUrl. Prefix the path with '~/'.</value>
<data name="InvalidRedirectPath" xml:space="preserve">
<value>Invalid '{0}'. The '{0}' can only be set relative to the AppUrl. Prefix the path with '~/'.</value>
</data>
<data name="MissingRequiredHeader" xml:space="preserve">
<value>The required header '{0}' is missing.</value>
Expand Down

0 comments on commit 8c87afb

Please sign in to comment.