Skip to content

Commit 8c87afb

Browse files
author
N. Taylor Mullen
committed
Facebook: Add cannot create cookie error page.
- 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.
1 parent d9f8a53 commit 8c87afb

File tree

7 files changed

+131
-25
lines changed

7 files changed

+131
-25
lines changed

src/Microsoft.AspNet.Facebook/Authorization/FacebookAuthorizeFilter.cs

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ namespace Microsoft.AspNet.Facebook.Authorization
1818
/// </summary>
1919
public class FacebookAuthorizeFilter : IAuthorizationFilter
2020
{
21-
private static readonly Uri DefaultAuthorizationRedirectUrl = new Uri("https://www.facebook.com/");
22-
private const string ExecuteMethodCannotBeCalledFormat = "The {0} execute method should not be called.";
21+
private static readonly Uri FacebookUri = new Uri("https://www.facebook.com/");
22+
private static readonly Uri DefaultAuthorizationRedirectUrl = FacebookUri;
23+
private static readonly Uri DefaultCannotCreateCookiesRedirectPath = FacebookUri;
24+
// Facebook Missing Permissions, shortened version to not add excessively long query string parameters to the url.
25+
private const string MissingPermissionsQueryName = "__fb_mps";
2326

2427
private FacebookConfiguration _config;
2528

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

7881
NameValueCollection parsedQueries = HttpUtility.ParseQueryString(request.Url.Query);
7982
HashSet<string> requiredPermissions = PermissionHelper.GetRequiredPermissions(authorizeAttributes);
83+
8084
bool handleError = !String.IsNullOrEmpty(parsedQueries["error"]);
8185

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

148+
// Add a query string parameter that enables us to identify if we've already prompted for missing permissions
149+
// and therefore can detect cookies.
150+
AddCookieVerificationQuery(parsedQueries);
151+
// Rebuild the redirect Url so it contains the new query string parameter.
152+
redirectUrl = GetRedirectUrl(request, parsedQueries);
153+
144154
PromptMissingPermissions(permissionContext, redirectUrl);
145155
}
146156
}
147157
}
148158

159+
/// <summary>
160+
/// Adds a query string parameter to a <see cref="NameValueCollection"/> that enables a
161+
/// <see cref="FacebookAuthorizeFilter"/> to detect when cookies are unavailable and then trigger the
162+
/// <see cref="OnCannotCreateCookies"/> hook.
163+
/// </summary>
164+
/// <param name="queries">List of query parameters that are used to create a url.</param>
165+
protected static void AddCookieVerificationQuery(NameValueCollection queries)
166+
{
167+
// Add a query string parameter that enables us to identify if we've already prompted for missing permissions
168+
queries.Add(MissingPermissionsQueryName, String.Empty);
169+
}
170+
149171
/// <summary>
150172
/// Called when authorization fails and need to create a redirect result.
151173
/// </summary>
@@ -176,6 +198,27 @@ protected ShowPromptResult ShowPrompt(PermissionContext context)
176198
return new ShowPromptResult(navigationUrl);
177199
}
178200

201+
/// <summary>
202+
/// Invoked during <see cref="OnAuthorization"/> after determining that cookies cannot be created. Default behavior
203+
/// redirects to a no cookies error page.
204+
/// </summary>
205+
/// <param name="context">Provides access to permission information associated with the user.</param>
206+
protected virtual void OnCannotCreateCookies(PermissionContext context)
207+
{
208+
Uri redirectPath;
209+
210+
if (String.IsNullOrEmpty(_config.CannotCreateCookieRedirectPath))
211+
{
212+
redirectPath = DefaultCannotCreateCookiesRedirectPath;
213+
}
214+
else
215+
{
216+
redirectPath = GetCannotCreateCookiesUri();
217+
}
218+
219+
context.Result = CreateRedirectResult(redirectPath);
220+
}
221+
179222
/// <summary>
180223
/// Invoked during <see cref="OnAuthorization"/> when a prompt requests permissions that were skipped or revoked.
181224
/// Set the <paramref name="context"/>'s Result property to modify login flow.
@@ -213,6 +256,14 @@ private Uri GetErroredAuthorizeUri(string originUrl, HashSet<string> requiredPer
213256
return authorizationUrlBuilder.Uri;
214257
}
215258

259+
private Uri GetCannotCreateCookiesUri()
260+
{
261+
UriBuilder noCookiesUrlBuilder = new UriBuilder(new Uri(_config.AppUrl));
262+
noCookiesUrlBuilder.Path += _config.CannotCreateCookieRedirectPath.Substring(1);
263+
264+
return noCookiesUrlBuilder.Uri;
265+
}
266+
216267
private string GetRedirectUrl(HttpRequestBase request)
217268
{
218269
NameValueCollection queryNameValuePair = HttpUtility.ParseQueryString(request.Url.Query);
@@ -273,21 +324,32 @@ private void PromptMissingPermissions(PermissionContext permissionContext, strin
273324

274325
permissionContext.RedirectUrl = redirectUrl;
275326

276-
// The DeniedPermissionPromptHook will only be invoked if we detect there are denied permissions.
277-
// It is attempted instead of the permission hook to allow app creators to handle situations when a user
278-
// skip's or revokes previously prompted permissions. Ex: redirect to a different page.
279-
if (deniedPermissions)
327+
// See if our persisted permissions cookie doesn't exist and if we've had missing permissions before.
328+
// Essentially this checks to see if we've tried to persist permissions before and were unsuccessful due to an
329+
// inability to create cookies.
330+
if (!PermissionHelper.RequestedPermissionsCookieExists(filterContext.HttpContext.Request) &&
331+
filterContext.HttpContext.Request.QueryString.Get(MissingPermissionsQueryName) != null)
280332
{
281-
OnDeniedPermissionPrompt(permissionContext);
333+
OnCannotCreateCookies(permissionContext);
282334
}
283335
else
284336
{
285-
OnPermissionPrompt(permissionContext);
286-
}
337+
// The DeniedPermissionPromptHook will only be invoked if we detect there are denied permissions.
338+
// It is attempted instead of the permission hook to allow app creators to handle situations when a user
339+
// skip's or revokes previously prompted permissions. Ex: redirect to a different page.
340+
if (deniedPermissions)
341+
{
342+
OnDeniedPermissionPrompt(permissionContext);
343+
}
344+
else
345+
{
346+
OnPermissionPrompt(permissionContext);
347+
}
287348

288-
// We persist the requested permissions in a cookie to know if a permission was denied in any way.
289-
// The persisted data allows us to detect skipping of permissions.
290-
PermissionHelper.PersistRequestedPermissions(filterContext, requiredPermissions);
349+
// We persist the requested permissions in a cookie to know if a permission was denied in any way.
350+
// The persisted data allows us to detect skipping of permissions.
351+
PermissionHelper.PersistRequestedPermissions(filterContext, requiredPermissions);
352+
}
291353

292354
filterContext.Result = permissionContext.Result;
293355
}

src/Microsoft.AspNet.Facebook/FacebookAppSettingKeys.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ internal static class FacebookAppSettingKeys
1010
public static readonly string AppNamespace = "Facebook:AppNamespace";
1111
public static readonly string AppUrl = "Facebook:AppUrl";
1212
public static readonly string AuthorizationRedirectPath = "Facebook:AuthorizationRedirectPath";
13+
public static readonly string CannotCreateCookiesRedirectPath = "Facebook:CannotCreateCookiesRedirectPath";
1314
}
1415
}

src/Microsoft.AspNet.Facebook/FacebookConfiguration.cs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class FacebookConfiguration
1818
private readonly ConcurrentDictionary<object, object> _properties = new ConcurrentDictionary<object, object>();
1919
private string _appUrl;
2020
private string _authorizationRedirectPath;
21+
private string _cannotCreateCookieRedirectPath;
2122

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

3738
/// <summary>
38-
/// 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.
39+
/// Gets or sets the URL path that the <see cref="Microsoft.AspNet.Facebook.Authorization.FacebookAuthorizeFilter"/> will
40+
/// redirect to when the user did not grant the required permissions. If value is not set it will result in a redirection
41+
/// to Facebook's home page.
3942
/// </summary>
4043
public string AuthorizationRedirectPath
4144
{
@@ -45,15 +48,30 @@ public string AuthorizationRedirectPath
4548
}
4649
set
4750
{
48-
// Check for '~/' prefix while allowing null or empty value to be set.
49-
if (!String.IsNullOrEmpty(value) && !value.StartsWith("~/", StringComparison.Ordinal))
50-
{
51-
throw new ArgumentException(Resources.InvalidAuthorizationRedirectPath, "value");
52-
}
51+
EnsureRedirectPath(value, "AuthorizationRedirectPath");
5352
_authorizationRedirectPath = value;
5453
}
5554
}
5655

56+
/// <summary>
57+
/// Gets or sets the URL path that the <see cref="Microsoft.AspNet.Facebook.Authorization.FacebookAuthorizeFilter"/> will
58+
/// redirect to when the we determine that we are unable to create cookies. If value is not set it will result in a
59+
/// redirection to Facebook's home page.
60+
/// </summary>
61+
public string CannotCreateCookieRedirectPath
62+
{
63+
get
64+
{
65+
return _cannotCreateCookieRedirectPath;
66+
}
67+
set
68+
{
69+
EnsureRedirectPath(value, "CannotCreateCookieRedirectPath");
70+
71+
_cannotCreateCookieRedirectPath = value;
72+
}
73+
}
74+
5775
/// <summary>
5876
/// Gets or sets the absolute URL for the Facebook App.
5977
/// </summary>
@@ -126,6 +144,22 @@ public virtual void LoadFromAppSettings()
126144
AppNamespace = ConfigurationManager.AppSettings[FacebookAppSettingKeys.AppNamespace];
127145
AppUrl = ConfigurationManager.AppSettings[FacebookAppSettingKeys.AppUrl];
128146
AuthorizationRedirectPath = ConfigurationManager.AppSettings[FacebookAppSettingKeys.AuthorizationRedirectPath];
147+
CannotCreateCookieRedirectPath =
148+
ConfigurationManager.AppSettings[FacebookAppSettingKeys.CannotCreateCookiesRedirectPath];
149+
}
150+
151+
private static void EnsureRedirectPath(string value, string redirectParameterName)
152+
{
153+
// Check for '~/' prefix while allowing null or empty value to be set.
154+
if (!String.IsNullOrEmpty(value) && !value.StartsWith("~/", StringComparison.Ordinal))
155+
{
156+
throw new ArgumentException(
157+
String.Format(
158+
CultureInfo.CurrentCulture,
159+
Resources.InvalidRedirectPath,
160+
redirectParameterName),
161+
"value");
162+
}
129163
}
130164

131165
private string GetAppUrl()

src/Microsoft.AspNet.Facebook/JavaScriptRedirectResult.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,21 @@ namespace Microsoft.AspNet.Facebook
1212
/// </summary>
1313
public class JavaScriptRedirectResult : ContentResult
1414
{
15+
internal Uri RedirectUrl { get; private set; }
16+
1517
/// <summary>
1618
/// Creates a JavaScript based redirect <see cref="ActionResult"/>.
1719
/// </summary>
1820
/// <param name="redirectUrl">The url to redirect to.</param>
1921
public JavaScriptRedirectResult(Uri redirectUrl)
2022
{
23+
RedirectUrl = redirectUrl;
24+
2125
ContentType = "text/html";
2226
Content = String.Format(
2327
CultureInfo.InvariantCulture,
2428
"<script>window.top.location = '{0}';</script>",
25-
HttpUtility.JavaScriptStringEncode(redirectUrl.AbsoluteUri));
29+
HttpUtility.JavaScriptStringEncode(RedirectUrl.AbsoluteUri));
2630
}
2731
}
2832
}

src/Microsoft.AspNet.Facebook/PermissionHelper.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ public static void PersistRequestedPermissions(AuthorizationContext context, IEn
7373
responseCookies.Add(cookie);
7474
}
7575

76+
public static bool RequestedPermissionsCookieExists(HttpRequestBase request)
77+
{
78+
return request.Cookies.Get(RequestedPermissionCookieName) != null;
79+
}
80+
7681
private static IEnumerable<string> GetPermissionsWithStatus(PermissionsStatus permissionsStatus,
7782
PermissionStatus status)
7883
{

src/Microsoft.AspNet.Facebook/Resources.Designer.cs

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.AspNet.Facebook/Resources.resx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,8 @@
126126
<data name="CircularReferenceNotSupported" xml:space="preserve">
127127
<value>Circular type references are not supported.</value>
128128
</data>
129-
<data name="InvalidAuthorizationRedirectPath" xml:space="preserve">
130-
<value>Invalid AuthorizationRedirectPath. The AuthorizationRedirectPath can only be set relative to the AppUrl. Prefix the path with '~/'.</value>
129+
<data name="InvalidRedirectPath" xml:space="preserve">
130+
<value>Invalid '{0}'. The '{0}' can only be set relative to the AppUrl. Prefix the path with '~/'.</value>
131131
</data>
132132
<data name="MissingRequiredHeader" xml:space="preserve">
133133
<value>The required header '{0}' is missing.</value>

0 commit comments

Comments
 (0)