From ea35ea1af5b82a5c098b473c77730ad87a7db909 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 2 Dec 2013 17:20:50 +1100 Subject: [PATCH] getting csrf stuff coded up, it's pretty much done just need to write a couple tests and add the filter to the necessary controller/actions --- .../Security/AuthenticationExtensions.cs | 22 ++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 2 + .../Editors/AuthenticationController.cs | 8 ++- .../Editors/UpdateCheckController.cs | 1 + src/Umbraco.Web/Umbraco.Web.csproj | 5 ++ .../Filters/AngularAntiForgeryHelper.cs | 56 +++++++++++++++++++ .../ClearAngularAntiForgeryTokenAttribute.cs | 25 +++++++++ .../SetAngularAntiForgeryTokenAttribute.cs | 33 +++++++++++ .../UmbracoBackOfficeLogoutAttribute.cs | 22 ++++++++ ...alidateAngularAntiForgeryTokenAttribute.cs | 28 ++++++++++ .../WebApi/HttpRequestMessageExtensions.cs | 3 +- .../umbraco.businesslogic.csproj | 1 + 12 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs create mode 100644 src/Umbraco.Web/WebApi/Filters/ClearAngularAntiForgeryTokenAttribute.cs create mode 100644 src/Umbraco.Web/WebApi/Filters/SetAngularAntiForgeryTokenAttribute.cs create mode 100644 src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs create mode 100644 src/Umbraco.Web/WebApi/Filters/ValidateAngularAntiForgeryTokenAttribute.cs diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 71ab76e082..1f78a431b6 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Net.Http; +using System.Net.Http.Headers; using System.Security.Principal; using System.Threading; using System.Web; @@ -127,6 +129,26 @@ namespace Umbraco.Core.Security Logout(http, UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName); } + /// + /// This clears the forms authentication cookie + /// + /// + public static void UmbracoLogout(this HttpResponseMessage response) + { + if (response == null) throw new ArgumentNullException("response"); + //remove the cookie + var cookie = new CookieHeaderValue(UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, "") + { + Expires = DateTime.Now.AddYears(-1), + Path = "/" + }; + response.Headers.AddCookies(new[] { cookie }); + } + + /// + /// This clears the forms authentication cookie + /// + /// internal static void UmbracoLogout(this HttpContext http) { if (http == null) throw new ArgumentNullException("http"); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index a216a588ac..88ac584da9 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -72,6 +72,8 @@ ..\packages\SqlServerCE.4.0.0.0\lib\System.Data.SqlServerCe.Entity.dll + + diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 1dc3f6cde8..8e1bdaf201 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Web; +using System.Web.Helpers; using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Security; @@ -103,6 +104,7 @@ namespace Umbraco.Web.Editors /// /// /// + [SetAngularAntiForgeryToken] public UserDetail PostLogin(string username, string password) { if (UmbracoContext.Security.ValidateBackOfficeCredentials(username, password)) @@ -111,6 +113,7 @@ namespace Umbraco.Web.Editors //TODO: Clean up the int cast! var timeoutSeconds = UmbracoContext.Security.PerformLogin((int)user.Id); + var result = Mapper.Map(user); //set their remaining seconds result.SecondsUntilTimeout = timeoutSeconds; @@ -129,9 +132,10 @@ namespace Umbraco.Web.Editors /// Logs the current user out /// /// + [UmbracoBackOfficeLogout] + [ClearAngularAntiForgeryToken] public HttpResponseMessage PostLogout() - { - UmbracoContext.Security.ClearCurrentLogin(); + { return Request.CreateResponse(HttpStatusCode.OK); } } diff --git a/src/Umbraco.Web/Editors/UpdateCheckController.cs b/src/Umbraco.Web/Editors/UpdateCheckController.cs index aa4966b14c..77f1a832fd 100644 --- a/src/Umbraco.Web/Editors/UpdateCheckController.cs +++ b/src/Umbraco.Web/Editors/UpdateCheckController.cs @@ -57,6 +57,7 @@ namespace Umbraco.Web.Editors //there is a result, set the outgoing cookie var cookie = new CookieHeaderValue("UMB_UPDCHK", "1") { + Path = "/", Expires = DateTimeOffset.Now.AddDays(GlobalSettings.VersionCheckPeriod), HttpOnly = true, Secure = GlobalSettings.UseSSL diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index c96f7a9501..a2b12ce893 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -440,7 +440,12 @@ + + + + + diff --git a/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs b/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs new file mode 100644 index 0000000000..ba98fb51f1 --- /dev/null +++ b/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs @@ -0,0 +1,56 @@ +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Web.Helpers; +using Umbraco.Core; + +namespace Umbraco.Web.WebApi.Filters +{ + public static class AngularAntiForgeryHelper + { + public const string CookieName = "XSRF-TOKEN"; + public const string Headername = "X-XSRF-TOKEN"; + + public static bool Validate(HttpRequestHeaders requestHeaders, out string failedReason) + { + failedReason = ""; + + if (requestHeaders.Any(z => StringExtensions.InvariantEquals(z.Key, Headername)) == false) + { + failedReason = "Missing token"; + return false; + } + + var headerToken = requestHeaders + .Where(z => z.Key.InvariantEquals(Headername)) + .Select(z => z.Value) + .SelectMany(z => z) + .FirstOrDefault(); + + var cookieToken = requestHeaders + .GetCookies() + .Select(c => c[CookieName]) + .FirstOrDefault(); + + // both header and cookie must be there + if (cookieToken == null || headerToken == null) + { + failedReason = "Missing token null"; + return false; + } + + // ensure that the cookie matches the header and then ensure it matches the correct value! + try + { + AntiForgery.Validate(cookieToken.Value, headerToken); + } + catch + { + failedReason = "Invalid token"; + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/ClearAngularAntiForgeryTokenAttribute.cs b/src/Umbraco.Web/WebApi/Filters/ClearAngularAntiForgeryTokenAttribute.cs new file mode 100644 index 0000000000..14994080d2 --- /dev/null +++ b/src/Umbraco.Web/WebApi/Filters/ClearAngularAntiForgeryTokenAttribute.cs @@ -0,0 +1,25 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Web.Http.Filters; + +namespace Umbraco.Web.WebApi.Filters +{ + public sealed class ClearAngularAntiForgeryTokenAttribute : ActionFilterAttribute + { + public override void OnActionExecuted(HttpActionExecutedContext context) + { + if (context.Response == null) return; + + //remove the cookie + var cookie = new CookieHeaderValue(AngularAntiForgeryHelper.CookieName, "null") + { + Expires = DateTime.Now.AddYears(-1), + //must be js readable + HttpOnly = false, + Path = "/" + }; + context.Response.Headers.AddCookies(new[] { cookie }); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/SetAngularAntiForgeryTokenAttribute.cs b/src/Umbraco.Web/WebApi/Filters/SetAngularAntiForgeryTokenAttribute.cs new file mode 100644 index 0000000000..07c6474032 --- /dev/null +++ b/src/Umbraco.Web/WebApi/Filters/SetAngularAntiForgeryTokenAttribute.cs @@ -0,0 +1,33 @@ +using System.Net.Http; +using System.Net.Http.Headers; +using System.Web.Helpers; +using System.Web.Http.Filters; +using Umbraco.Core.Configuration; + +namespace Umbraco.Web.WebApi.Filters +{ + /// + /// A filter to set the csrf cookie token based on angular conventions + /// + public sealed class SetAngularAntiForgeryTokenAttribute : ActionFilterAttribute + { + public override void OnActionExecuted(HttpActionExecutedContext context) + { + if (context.Response == null) return; + + string cookieToken, formToken; + AntiForgery.GetTokens(null, out cookieToken, out formToken); + + //there is a result, set the outgoing cookie + var cookie = new CookieHeaderValue(AngularAntiForgeryHelper.CookieName, cookieToken) + { + Path = "/", + //must be js readable + HttpOnly = false, + Secure = GlobalSettings.UseSSL + }; + context.Response.Headers.AddCookies(new[] { cookie }); + + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs b/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs new file mode 100644 index 0000000000..e8c37c81c8 --- /dev/null +++ b/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs @@ -0,0 +1,22 @@ +using System.Web.Http.Filters; +using Umbraco.Core.Security; + +namespace Umbraco.Web.WebApi.Filters +{ + /// + /// A filter that is used to remove the authorization cookie for the current user + /// + /// + /// This is used so that we can log a user out in conjunction with using other filters that modify the cookies collection. + /// SD: I beleive this is a bug with web api since if you modify the cookies collection on the HttpContext.Current and then + /// use a filter to write the cookie headers, the filter seems to have no affect at all. + /// + public sealed class UmbracoBackOfficeLogoutAttribute : ActionFilterAttribute + { + public override void OnActionExecuted(HttpActionExecutedContext context) + { + if (context.Response == null) return; + context.Response.UmbracoLogout(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/ValidateAngularAntiForgeryTokenAttribute.cs b/src/Umbraco.Web/WebApi/Filters/ValidateAngularAntiForgeryTokenAttribute.cs new file mode 100644 index 0000000000..cdbe3de4ac --- /dev/null +++ b/src/Umbraco.Web/WebApi/Filters/ValidateAngularAntiForgeryTokenAttribute.cs @@ -0,0 +1,28 @@ +using System.Net; +using System.Net.Http; +using System.Web.Http.Filters; + +namespace Umbraco.Web.WebApi.Filters +{ + /// + /// A filter to check for the csrf token based on Angular's standard approach + /// + /// + /// Code derived from http://ericpanorel.net/2013/07/28/spa-authentication-and-csrf-mvc4-antiforgery-implementation/ + /// + public sealed class ValidateAngularAntiForgeryTokenAttribute : ActionFilterAttribute + { + public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext) + { + string failedReason; + if (AngularAntiForgeryHelper.Validate(actionContext.Request.Headers, out failedReason) == false) + { + actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.ExpectationFailed); + actionContext.Response.ReasonPhrase = failedReason; + return; + } + + base.OnActionExecuting(actionContext); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs b/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs index 7262d3b759..27c81f5c8d 100644 --- a/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs +++ b/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using System.Web; @@ -12,7 +13,7 @@ using Umbraco.Core; namespace Umbraco.Web.WebApi { - + public static class HttpRequestMessageExtensions { /// diff --git a/src/umbraco.businesslogic/umbraco.businesslogic.csproj b/src/umbraco.businesslogic/umbraco.businesslogic.csproj index 864cd78f9c..1d1d543f08 100644 --- a/src/umbraco.businesslogic/umbraco.businesslogic.csproj +++ b/src/umbraco.businesslogic/umbraco.businesslogic.csproj @@ -124,6 +124,7 @@ System.Data + System.Web