diff --git a/src/Umbraco.Web.BackOffice/Extensions/AngularAntiForgeryExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/AngularAntiForgeryExtensions.cs deleted file mode 100644 index a9e0c64751..0000000000 --- a/src/Umbraco.Web.BackOffice/Extensions/AngularAntiForgeryExtensions.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.Collections.Generic; -using Umbraco.Core; -using Microsoft.AspNetCore.Antiforgery; -using Microsoft.AspNetCore.Http; - -namespace Umbraco.Extensions -{ - /// - /// A helper class to deal with csrf prevention with angularjs and webapi - /// - public static class AngularAntiForgeryExtensions - { - /// - /// Returns 2 tokens - one for the cookie value and one that angular should set as the header value - /// - /// - /// - /// - /// .Net provides us a way to validate one token with another for added security. With the way angular works, this - /// means that we need to set 2 cookies since angular uses one cookie value to create the header value, then we want to validate - /// this header value against our original cookie value. - /// - public static void GetTokens(this IAntiforgery antiforgery, HttpContext httpContext, out string cookieToken, out string headerToken) - { - var result = antiforgery.GetTokens(httpContext); - - cookieToken = result.CookieToken; - headerToken = result.RequestToken; - } - - ///// - ///// Validates the header token against the validation cookie value - ///// - ///// - ///// - ///// - //public static bool ValidateTokens(this IAntiforgery antiforgery, HttpContext httpContext, string cookieToken, string headerToken) - //{ - // // ensure that the cookie matches the header and then ensure it matches the correct value! - // try - // { - // antiforgery.Va .Validate(cookieToken, headerToken); - // } - // catch (Exception ex) - // { - // Current.Logger.Error(typeof(AngularAntiForgeryHelper), ex, "Could not validate XSRF token"); - // return false; - // } - // return true; - //} - - //internal static bool ValidateHeaders( - // KeyValuePair>[] requestHeaders, - // string cookieToken, - // out string failedReason) - //{ - // failedReason = ""; - - // if (requestHeaders.Any(z => z.Key.InvariantEquals(Constants.Web.AngularHeadername)) == false) - // { - // failedReason = "Missing token"; - // return false; - // } - - // var headerToken = requestHeaders - // .Where(z => z.Key.InvariantEquals(Constants.Web.AngularHeadername)) - // .Select(z => z.Value) - // .SelectMany(z => z) - // .FirstOrDefault(); - - // // both header and cookie must be there - // if (cookieToken == null || headerToken == null) - // { - // failedReason = "Missing token null"; - // return false; - // } - - // if (ValidateTokens(cookieToken, headerToken) == false) - // { - // failedReason = "Invalid token"; - // return false; - // } - - // return true; - //} - - ///// - ///// Validates the headers/cookies passed in for the request - ///// - ///// - ///// - ///// - //public static bool ValidateHeaders(HttpRequestHeaders requestHeaders, out string failedReason) - //{ - // var cookieToken = requestHeaders.GetCookieValue(Constants.Web.CsrfValidationCookieName); - - // return ValidateHeaders( - // requestHeaders.ToDictionary(x => x.Key, x => x.Value).ToArray(), - // cookieToken == null ? null : cookieToken, - // out failedReason); - //} - - } -} diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs index e4b81d806f..5bf5224b07 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs @@ -11,6 +11,19 @@ namespace Umbraco.Extensions { public static class UmbracoBackOfficeServiceCollectionExtensions { + /// + /// Adds the services required for running the Umbraco back office + /// + /// + public static void AddUmbracoBackOffice(this IServiceCollection services) + { + services.AddAntiforgery(); + } + + /// + /// Adds the services required for using Umbraco back office Identity + /// + /// public static void AddUmbracoBackOfficeIdentity(this IServiceCollection services) { services.AddDataProtection(); diff --git a/src/Umbraco.Web.BackOffice/Filters/SetAngularAntiForgeryTokens.cs b/src/Umbraco.Web.BackOffice/Filters/SetAngularAntiForgeryTokens.cs index 8a830181cb..fdb07b47d2 100644 --- a/src/Umbraco.Web.BackOffice/Filters/SetAngularAntiForgeryTokens.cs +++ b/src/Umbraco.Web.BackOffice/Filters/SetAngularAntiForgeryTokens.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using Umbraco.Core; using Umbraco.Core.Configuration; +using Umbraco.Web.BackOffice.Security; namespace Umbraco.Extensions { @@ -14,10 +15,10 @@ namespace Umbraco.Extensions /// public sealed class SetAngularAntiForgeryTokens : IAsyncActionFilter { - private readonly IAntiforgery _antiforgery; + private readonly IBackOfficeAntiforgery _antiforgery; private readonly IGlobalSettings _globalSettings; - public SetAngularAntiForgeryTokens(IAntiforgery antiforgery, IGlobalSettings globalSettings) + public SetAngularAntiForgeryTokens(IBackOfficeAntiforgery antiforgery, IGlobalSettings globalSettings) { _antiforgery = antiforgery; _globalSettings = globalSettings; @@ -35,7 +36,8 @@ namespace Umbraco.Extensions && context.HttpContext.Request.Cookies.TryGetValue(Constants.Web.CsrfValidationCookieName, out var csrfCookieVal)) { //if they are not valid for some strange reason - we need to continue setting valid ones - if (await _antiforgery.IsRequestValidAsync(context.HttpContext)) + var valResult = await _antiforgery.ValidateHeadersAsync(context.HttpContext); + if (valResult.Success) { await next(); return; diff --git a/src/Umbraco.Web.BackOffice/Runtime/BackOfficeComposer.cs b/src/Umbraco.Web.BackOffice/Runtime/BackOfficeComposer.cs index 43aae82ed5..2826eddde1 100644 --- a/src/Umbraco.Web.BackOffice/Runtime/BackOfficeComposer.cs +++ b/src/Umbraco.Web.BackOffice/Runtime/BackOfficeComposer.cs @@ -17,6 +17,8 @@ namespace Umbraco.Web.BackOffice.Runtime composition.RegisterUnique(); composition.RegisterUnique(); composition.Register(Lifetime.Scope); + + composition.RegisterUnique(); } } } diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeAntiforgery.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeAntiforgery.cs new file mode 100644 index 0000000000..66e59ada6f --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeAntiforgery.cs @@ -0,0 +1,127 @@ +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Hosting; + +namespace Umbraco.Web.BackOffice.Security +{ + + /// + /// Antiforgery implementation for the Umbraco back office + /// + /// + /// This is a wrapper around the global/default .net service. Because this service is a single/global + /// object and all of it is internal we don't have the flexibility to create our own segregated service so we have to work around + /// that limitation by wrapping the default and doing a few tricks to have this segregated for the Back office only. + /// + public class BackOfficeAntiforgery : IBackOfficeAntiforgery + { + private readonly IAntiforgery _defaultAntiforgery; + private readonly IOptions _antiforgeryOptions; + + public BackOfficeAntiforgery( + IAntiforgery defaultAntiforgery, + IOptions antiforgeryOptions) + { + _defaultAntiforgery = defaultAntiforgery; + _antiforgeryOptions = antiforgeryOptions; + } + + public async Task ValidateTokensAsync(HttpContext httpContext, string cookieToken, string headerToken) + { + // We need to do some tricks here, save the initial cookie vals, then reset later + var originalCookies = httpContext.Request.Cookies; + var originalCookiesHeader = httpContext.Request.Headers[HeaderNames.Cookie]; + var originalHeader = httpContext.Request.Headers[_antiforgeryOptions.Value.HeaderName]; + //var originalForm = httpContext.Request.Form; + try + { + // this is how you write to the request cookies, it's the only way + var cookieHeaderVals = CookieHeaderValue.ParseList(originalCookiesHeader); + cookieHeaderVals.Add(new CookieHeaderValue(_antiforgeryOptions.Value.Cookie.Name, cookieToken)); + httpContext.Request.Headers[HeaderNames.Cookie] = cookieHeaderVals.Select(c => c.ToString()).ToArray(); + + // change the header/form val to ours + //var newForm = httpContext.Request.Form.ToDictionary(x => x.Key, x => x.Value); + //newForm.Add(_antiforgeryOptions.Value.FormFieldName, headerToken); + //httpContext.Request.Form = new FormCollection(newForm); + httpContext.Request.Headers[_antiforgeryOptions.Value.HeaderName] = headerToken; + + return await _defaultAntiforgery.IsRequestValidAsync(httpContext); + } + finally + { + // reset + var cookieHeaderVals = CookieHeaderValue.ParseList(originalCookiesHeader); + httpContext.Request.Headers[HeaderNames.Cookie] = cookieHeaderVals.Select(c => c.ToString()).ToArray(); + + if (originalHeader.Count > 0) + httpContext.Request.Headers[_antiforgeryOptions.Value.HeaderName] = originalHeader; + + //httpContext.Request.Form = originalForm; + } + } + + /// + /// Validates the headers/cookies passed in for the request + /// + /// + /// + /// + public async Task> ValidateHeadersAsync(HttpContext httpContext) + { + httpContext.Request.Cookies.TryGetValue(Constants.Web.CsrfValidationCookieName, out var cookieToken); + + return await ValidateHeadersAsync( + httpContext, + cookieToken == null ? null : cookieToken); + } + + private async Task> ValidateHeadersAsync( + HttpContext httpContext, + string cookieToken) + { + var requestHeaders = httpContext.Request.Headers; + if (!requestHeaders.TryGetValue(Constants.Web.AngularHeadername, out var headerVals)) + { + return Attempt.Fail("Missing token"); + } + + var headerToken = headerVals.FirstOrDefault(); + + // both header and cookie must be there + if (cookieToken == null || headerToken == null) + { + return Attempt.Fail("Missing token null"); + } + + if (await ValidateTokensAsync(httpContext, cookieToken, headerToken) == false) + { + return Attempt.Fail("Invalid token"); + } + + return Attempt.Succeed(); + } + + public void GetTokens(HttpContext httpContext, out string cookieToken, out string headerToken) + { + var set = _defaultAntiforgery.GetTokens(httpContext); + + cookieToken = set.RequestToken; + headerToken = set.RequestToken; + } + + + } +} diff --git a/src/Umbraco.Web.BackOffice/Security/IBackOfficeAntiforgery.cs b/src/Umbraco.Web.BackOffice/Security/IBackOfficeAntiforgery.cs new file mode 100644 index 0000000000..5b0f6e33dd --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Security/IBackOfficeAntiforgery.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Http; +using System.Threading.Tasks; +using Umbraco.Core; + +namespace Umbraco.Web.BackOffice.Security +{ + /// + /// Antiforgery implementation for the Umbraco back office + /// + public interface IBackOfficeAntiforgery //: IAntiforgery + { + Task> ValidateHeadersAsync(HttpContext httpContext); + Task ValidateTokensAsync(HttpContext httpContext, string cookieToken, string headerToken); + void GetTokens(HttpContext httpContext, out string cookieToken, out string headerToken); + } +} diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 547a320de1..c8dd845546 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -39,10 +39,13 @@ namespace Umbraco.Web.UI.BackOffice // relying on a global configuration set by a user since if a custom IControllerActivator is used for our own controllers we may not // guarantee it will work. And then... is that even possible? + // TODO: we will need to simplify this and prob just have a one or 2 main method that devs call which call all other required methods, + // but for now we'll just be explicit with all of them services.AddUmbracoConfiguration(_config); services.AddUmbracoCore(_env, out var factory); services.AddUmbracoWebComponents(); services.AddUmbracoRuntimeMinifier(_config); + services.AddUmbracoBackOffice(); services.AddUmbracoBackOfficeIdentity(); services.AddMvc();