diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs
index 5adf163bee..3b497bff86 100644
--- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs
+++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs
@@ -1,466 +1,67 @@
-using System;
-using System.Collections;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Collections.Specialized;
-using System.ComponentModel;
-using System.Globalization;
-using System.Linq;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Security.Claims;
-using System.Security.Principal;
-using System.Threading;
-using System.Web;
-using System.Web.Security;
-using Microsoft.AspNet.Identity;
-using AutoMapper;
-using Microsoft.Owin;
-using Newtonsoft.Json;
-using Umbraco.Core.Configuration;
-using Umbraco.Core.Composing;
-using Umbraco.Core.Models.Membership;
-using Umbraco.Core.Logging;
-using IUser = Umbraco.Core.Models.Membership.IUser;
-
-namespace Umbraco.Core.Security
-{
- ///
- /// Extensions to create and renew and remove authentication tickets for the Umbraco back office
- ///
- public static class AuthenticationExtensions
- {
- ///
- /// This will check the ticket to see if it is valid, if it is it will set the current thread's user and culture
- ///
- ///
- ///
- /// If true will attempt to renew the ticket
- public static bool AuthenticateCurrentRequest(this HttpContextBase http, FormsAuthenticationTicket ticket, bool renewTicket)
- {
- if (http == null) throw new ArgumentNullException("http");
-
- //if there was a ticket, it's not expired, - it should not be renewed or its renewable
- if (ticket != null && ticket.Expired == false && (renewTicket == false || http.RenewUmbracoAuthTicket()))
- {
- try
- {
- //create the Umbraco user identity
- var identity = new UmbracoBackOfficeIdentity(ticket);
-
- //set the principal object
- var principal = new GenericPrincipal(identity, identity.Roles);
-
- //It is actually not good enough to set this on the current app Context and the thread, it also needs
- // to be set explicitly on the HttpContext.Current !! This is a strange web api thing that is actually
- // an underlying fault of asp.net not propogating the User correctly.
- if (HttpContext.Current != null)
- {
- HttpContext.Current.User = principal;
- }
- http.User = principal;
- Thread.CurrentPrincipal = principal;
-
- //This is a back office request, we will also set the culture/ui culture
- Thread.CurrentThread.CurrentCulture =
- Thread.CurrentThread.CurrentUICulture =
- new System.Globalization.CultureInfo(identity.Culture);
-
- return true;
- }
- catch (Exception ex)
- {
- if (ex is FormatException || ex is JsonReaderException)
- {
- //this will occur if the cookie data is invalid
- http.UmbracoLogout();
- }
- else
- {
- throw;
- }
-
- }
- }
-
- return false;
- }
-
- ///
- /// This will return the current back office identity if the IPrincipal is the correct type
- ///
- ///
- ///
- internal static UmbracoBackOfficeIdentity GetUmbracoIdentity(this IPrincipal user)
- {
- //If it's already a UmbracoBackOfficeIdentity
- var backOfficeIdentity = user.Identity as UmbracoBackOfficeIdentity;
- if (backOfficeIdentity != null) return backOfficeIdentity;
-
- //Check if there's more than one identity assigned and see if it's a UmbracoBackOfficeIdentity and use that
- var claimsPrincipal = user as ClaimsPrincipal;
- if (claimsPrincipal != null)
- {
- backOfficeIdentity = claimsPrincipal.Identities.OfType().FirstOrDefault();
- if (backOfficeIdentity != null) return backOfficeIdentity;
- }
-
- //Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd and has the back office session
- var claimsIdentity = user.Identity as ClaimsIdentity;
- if (claimsIdentity != null && claimsIdentity.IsAuthenticated && claimsIdentity.HasClaim(x => x.Type == Constants.Security.SessionIdClaimType))
- {
- try
- {
- return UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity);
- }
- catch (InvalidOperationException)
- {
- }
- }
-
- return null;
- }
-
- ///
- /// This will return the current back office identity.
- ///
- ///
- ///
- /// If set to true and a back office identity is not found and not authenticated, this will attempt to authenticate the
- /// request just as is done in the Umbraco module and then set the current identity if it is valid.
- /// Just like in the UmbracoModule, if this is true then the user's culture will be assigned to the request.
- ///
- ///
- /// Returns the current back office identity if an admin is authenticated otherwise null
- ///
- public static UmbracoBackOfficeIdentity GetCurrentIdentity(this HttpContextBase http, bool authenticateRequestIfNotFound)
- {
- if (http == null) throw new ArgumentNullException("http");
- if (http.User == null) return null; //there's no user at all so no identity
-
- //If it's already a UmbracoBackOfficeIdentity
- var backOfficeIdentity = GetUmbracoIdentity(http.User);
- if (backOfficeIdentity != null) return backOfficeIdentity;
-
- if (authenticateRequestIfNotFound == false) return null;
-
- //even if authenticateRequestIfNotFound is true we cannot continue if the request is actually authenticated
- // which would mean something strange is going on that it is not an umbraco identity.
- if (http.User.Identity.IsAuthenticated) return null;
-
- //So the user is not authed but we've been asked to do the auth if authenticateRequestIfNotFound = true,
- // which might occur in old webforms style things or for routes that aren't included as a back office request.
- // in this case, we are just reverting to authing using the cookie.
-
- // TODO: Even though this is in theory legacy, we have legacy bits laying around and we'd need to do the auth based on
- // how the Module will eventually do it (by calling in to any registered authenticators).
-
- var ticket = http.GetUmbracoAuthTicket();
- if (http.AuthenticateCurrentRequest(ticket, true))
- {
- //now we 'should have an umbraco identity
- return http.User.Identity as UmbracoBackOfficeIdentity;
- }
- return null;
- }
-
- ///
- /// This will return the current back office identity.
- ///
- ///
- ///
- /// If set to true and a back office identity is not found and not authenticated, this will attempt to authenticate the
- /// request just as is done in the Umbraco module and then set the current identity if it is valid
- ///
- ///
- /// Returns the current back office identity if an admin is authenticated otherwise null
- ///
- internal static UmbracoBackOfficeIdentity GetCurrentIdentity(this HttpContext http, bool authenticateRequestIfNotFound)
- {
- if (http == null) throw new ArgumentNullException("http");
- return new HttpContextWrapper(http).GetCurrentIdentity(authenticateRequestIfNotFound);
- }
-
- public static void UmbracoLogout(this HttpContextBase http)
- {
- if (http == null) throw new ArgumentNullException("http");
- Logout(http, UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName);
- }
-
- ///
- /// This clears the forms authentication cookie
- ///
- ///
- internal static void UmbracoLogout(this HttpContext http)
- {
- if (http == null) throw new ArgumentNullException("http");
- new HttpContextWrapper(http).UmbracoLogout();
- }
-
- ///
- /// This will force ticket renewal in the OWIN pipeline
- ///
- ///
- ///
- public static bool RenewUmbracoAuthTicket(this HttpContextBase http)
- {
- if (http == null) throw new ArgumentNullException("http");
- http.Items[Constants.Security.ForceReAuthFlag] = true;
- return true;
- }
-
- ///
- /// This will force ticket renewal in the OWIN pipeline
- ///
- ///
- ///
- internal static bool RenewUmbracoAuthTicket(this HttpContext http)
- {
- if (http == null) throw new ArgumentNullException("http");
- http.Items[Constants.Security.ForceReAuthFlag] = true;
- return true;
- }
-
- ///
- /// Creates the umbraco authentication ticket
- ///
- ///
- ///
- public static FormsAuthenticationTicket CreateUmbracoAuthTicket(this HttpContextBase http, UserData userdata)
- {
- //ONLY used by BasePage.doLogin!
-
- if (http == null) throw new ArgumentNullException("http");
- if (userdata == null) throw new ArgumentNullException("userdata");
-
- var userDataString = JsonConvert.SerializeObject(userdata);
- return CreateAuthTicketAndCookie(
- http,
- userdata.Username,
- userDataString,
- //use the configuration timeout - this is the same timeout that will be used when renewing the ticket.
- GlobalSettings.TimeOutInMinutes,
- //Umbraco has always persisted it's original cookie for 1 day so we'll keep it that way
- 1440,
- UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName,
- UmbracoConfig.For.UmbracoSettings().Security.AuthCookieDomain);
- }
-
- ///
- /// returns the number of seconds the user has until their auth session times out
- ///
- ///
- ///
- public static double GetRemainingAuthSeconds(this HttpContextBase http)
- {
- if (http == null) throw new ArgumentNullException("http");
- var ticket = http.GetUmbracoAuthTicket();
- return ticket.GetRemainingAuthSeconds();
- }
-
- ///
- /// returns the number of seconds the user has until their auth session times out
- ///
- ///
- ///
- public static double GetRemainingAuthSeconds(this FormsAuthenticationTicket ticket)
- {
- if (ticket == null)
- {
- return 0;
- }
- var utcExpired = ticket.Expiration.ToUniversalTime();
- var secondsRemaining = utcExpired.Subtract(DateTime.UtcNow).TotalSeconds;
- return secondsRemaining;
- }
-
- ///
- /// Gets the umbraco auth ticket
- ///
- ///
- ///
- public static FormsAuthenticationTicket GetUmbracoAuthTicket(this HttpContextBase http)
- {
- if (http == null) throw new ArgumentNullException("http");
- return GetAuthTicket(http, UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName);
- }
-
- internal static FormsAuthenticationTicket GetUmbracoAuthTicket(this HttpContext http)
- {
- if (http == null) throw new ArgumentNullException("http");
- return new HttpContextWrapper(http).GetUmbracoAuthTicket();
- }
-
- internal static FormsAuthenticationTicket GetUmbracoAuthTicket(this IOwinContext ctx)
- {
- if (ctx == null) throw new ArgumentNullException("ctx");
- //get the ticket
- try
- {
- return GetAuthTicket(ctx.Request.Cookies.ToDictionary(x => x.Key, x => x.Value), UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName);
- }
- catch (Exception)
- {
- ctx.Authentication.SignOut(
- Constants.Security.BackOfficeAuthenticationType,
- Constants.Security.BackOfficeExternalAuthenticationType);
- return null;
- }
- }
-
- ///
- /// This clears the forms authentication cookie
- ///
- ///
- ///
- private static void Logout(this HttpContextBase http, string cookieName)
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Security.Claims;
+using System.Security.Principal;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Umbraco.Core.Security
+{
+ public static class AuthenticationExtensions
+ {
+ ///
+ /// This will return the current back office identity if the IPrincipal is the correct type
+ ///
+ ///
+ ///
+ internal static UmbracoBackOfficeIdentity GetUmbracoIdentity(this IPrincipal user)
{
- //We need to clear the sessionId from the database. This is legacy code to do any logging out and shouldn't really be used at all but in any case
- //we need to make sure the session is cleared. Due to the legacy nature of this it means we need to use singletons
- if (http.User != null)
- {
- var claimsIdentity = http.User.Identity as ClaimsIdentity;
- if (claimsIdentity != null)
- {
- var sessionId = claimsIdentity.FindFirstValue(Constants.Security.SessionIdClaimType);
- Guid guidSession;
- if (sessionId.IsNullOrWhiteSpace() == false && Guid.TryParse(sessionId, out guidSession))
- {
- Current.Services.UserService.ClearLoginSession(guidSession);
- }
+ //If it's already a UmbracoBackOfficeIdentity
+ if (user.Identity is UmbracoBackOfficeIdentity backOfficeIdentity) return backOfficeIdentity;
+
+ //Check if there's more than one identity assigned and see if it's a UmbracoBackOfficeIdentity and use that
+ if (user is ClaimsPrincipal claimsPrincipal)
+ {
+ backOfficeIdentity = claimsPrincipal.Identities.OfType().FirstOrDefault();
+ if (backOfficeIdentity != null) return backOfficeIdentity;
+ }
+
+ //Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd and has the back office session
+ if (user.Identity is ClaimsIdentity claimsIdentity && claimsIdentity.IsAuthenticated && claimsIdentity.HasClaim(x => x.Type == Constants.Security.SessionIdClaimType))
+ {
+ try
+ {
+ return UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity);
+ }
+ catch (InvalidOperationException)
+ {
}
}
-
- if (http == null) throw new ArgumentNullException("http");
- //clear the preview cookie and external login
- var cookies = new[] { cookieName, Constants.Web.PreviewCookieName, Constants.Security.BackOfficeExternalCookieName };
- foreach (var c in cookies)
- {
- //remove from the request
- http.Request.Cookies.Remove(c);
-
- //expire from the response
- var formsCookie = http.Response.Cookies[c];
- if (formsCookie != null)
- {
- //this will expire immediately and be removed from the browser
- formsCookie.Expires = DateTime.Now.AddYears(-1);
- }
- else
- {
- //ensure there's def an expired cookie
- http.Response.Cookies.Add(new HttpCookie(c) { Expires = DateTime.Now.AddYears(-1) });
- }
- }
- }
-
- private static FormsAuthenticationTicket GetAuthTicket(this HttpContextBase http, string cookieName)
- {
- var asDictionary = new Dictionary();
- for (var i = 0; i < http.Request.Cookies.Keys.Count; i++)
- {
- var key = http.Request.Cookies.Keys.Get(i);
- asDictionary[key] = http.Request.Cookies[key].Value;
- }
-
- //get the ticket
- try
- {
-
- return GetAuthTicket(asDictionary, cookieName);
- }
- catch (Exception)
- {
- //occurs when decryption fails
- http.Logout(cookieName);
- return null;
- }
- }
-
- private static FormsAuthenticationTicket GetAuthTicket(IDictionary cookies, string cookieName)
- {
- if (cookies == null) throw new ArgumentNullException("cookies");
-
- if (cookies.ContainsKey(cookieName) == false) return null;
-
- var formsCookie = cookies[cookieName];
- if (formsCookie == null)
- {
- return null;
- }
- //get the ticket
- return FormsAuthentication.Decrypt(formsCookie);
- }
-
- ///
- /// Creates a custom FormsAuthentication ticket with the data specified
- ///
- /// The HTTP.
- /// The username.
- /// The user data.
- /// The login timeout mins.
- /// The minutes persisted.
- /// Name of the cookie.
- /// The cookie domain.
- private static FormsAuthenticationTicket CreateAuthTicketAndCookie(this HttpContextBase http,
- string username,
- string userData,
- int loginTimeoutMins,
- int minutesPersisted,
- string cookieName,
- string cookieDomain)
- {
- if (http == null) throw new ArgumentNullException("http");
- // Create a new ticket used for authentication
- var ticket = new FormsAuthenticationTicket(
- 4,
- username,
- DateTime.Now,
- DateTime.Now.AddMinutes(loginTimeoutMins),
- true,
- userData,
- "/"
- );
-
- // Encrypt the cookie using the machine key for secure transport
- var hash = FormsAuthentication.Encrypt(ticket);
- var cookie = new HttpCookie(
- cookieName,
- hash)
- {
- Expires = DateTime.Now.AddMinutes(minutesPersisted),
- Domain = cookieDomain,
- Path = "/"
- };
-
- if (GlobalSettings.UseSSL)
- cookie.Secure = true;
-
- //ensure http only, this should only be able to be accessed via the server
- cookie.HttpOnly = true;
-
- http.Response.Cookies.Set(cookie);
-
- return ticket;
- }
- ///
- /// Ensures that the thread culture is set based on the back office user's culture
- ///
- ///
- internal static void EnsureCulture(this IIdentity identity)
- {
- if (identity is UmbracoBackOfficeIdentity umbIdentity && umbIdentity.IsAuthenticated)
- {
- Thread.CurrentThread.CurrentUICulture =
- Thread.CurrentThread.CurrentCulture =
- UserCultures.GetOrAdd(umbIdentity.Culture, s => new CultureInfo(s));
- }
+ return null;
}
-
- ///
- /// Used so that we aren't creating a new CultureInfo object for every single request
- ///
- private static readonly ConcurrentDictionary UserCultures = new ConcurrentDictionary();
- }
-}
+
+ ///
+ /// Ensures that the thread culture is set based on the back office user's culture
+ ///
+ ///
+ internal static void EnsureCulture(this IIdentity identity)
+ {
+ if (identity is UmbracoBackOfficeIdentity umbIdentity && umbIdentity.IsAuthenticated)
+ {
+ Thread.CurrentThread.CurrentUICulture =
+ Thread.CurrentThread.CurrentCulture = UserCultures.GetOrAdd(umbIdentity.Culture, s => new CultureInfo(s));
+ }
+ }
+
+
+ ///
+ /// Used so that we aren't creating a new CultureInfo object for every single request
+ ///
+ private static readonly ConcurrentDictionary UserCultures = new ConcurrentDictionary();
+ }
+}
diff --git a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs
index cec1ee6bcb..490e667c17 100644
--- a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs
+++ b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs
@@ -1,4 +1,5 @@
-using System.Linq;
+using System;
+using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
@@ -25,22 +26,20 @@ namespace Umbraco.Core.Security
var baseIdentity = await base.CreateAsync(manager, user, authenticationType);
var umbracoIdentity = new UmbracoBackOfficeIdentity(baseIdentity,
+ user.Id,
+ user.UserName,
+ user.Name,
+ user.CalculatedContentStartNodeIds,
+ user.CalculatedMediaStartNodeIds,
+ user.Culture,
//NOTE - there is no session id assigned here, this is just creating the identity, a session id will be generated when the cookie is written
- new UserData
- {
- Id = user.Id,
- Username = user.UserName,
- RealName = user.Name,
- AllowedApplications = user.AllowedSections,
- Culture = user.Culture,
- Roles = user.Roles.Select(x => x.RoleId).ToArray(),
- StartContentNodes = user.CalculatedContentStartNodeIds,
- StartMediaNodes = user.CalculatedMediaStartNodeIds,
- SecurityStamp = user.SecurityStamp
- });
+ Guid.NewGuid().ToString(),
+ user.SecurityStamp,
+ user.AllowedSections,
+ user.Roles.Select(x => x.RoleId).ToArray());
return umbracoIdentity;
- }
+ }
}
public class BackOfficeClaimsIdentityFactory : BackOfficeClaimsIdentityFactory
diff --git a/src/Umbraco.Core/Security/BackOfficeCookieAuthenticationProvider.cs b/src/Umbraco.Core/Security/BackOfficeCookieAuthenticationProvider.cs
index b75dd76e47..a52f720b53 100644
--- a/src/Umbraco.Core/Security/BackOfficeCookieAuthenticationProvider.cs
+++ b/src/Umbraco.Core/Security/BackOfficeCookieAuthenticationProvider.cs
@@ -28,11 +28,11 @@ namespace Umbraco.Core.Security
//create a session token - if we are configured and not in an upgrade state then use the db, otherwise just generate one
var session = RuntimeState.Level == RuntimeLevel.Run
- ? UserService.CreateLoginSession((int)backOfficeIdentity.Id, context.OwinContext.GetCurrentRequestIpAddress())
+ ? UserService.CreateLoginSession(backOfficeIdentity.Id, context.OwinContext.GetCurrentRequestIpAddress())
: Guid.NewGuid();
- backOfficeIdentity.UserData.SessionId = session.ToString();
- }
+ backOfficeIdentity.SessionId = session.ToString();
+ }
base.ResponseSignIn(context);
}
diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs
index 32f7d3bd8f..4c651e9432 100644
--- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs
+++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs
@@ -31,16 +31,6 @@ namespace Umbraco.Core.Security
{
}
- [EditorBrowsable(EditorBrowsableState.Never)]
- [Obsolete("Use the constructor specifying all dependencies instead")]
- public BackOfficeUserManager(
- IUserStore store,
- IdentityFactoryOptions options,
- MembershipProviderBase membershipProvider)
- : this(store, options, membershipProvider, UmbracoConfig.For.UmbracoSettings().Content)
- {
- }
-
public BackOfficeUserManager(
IUserStore store,
IdentityFactoryOptions options,
@@ -84,17 +74,6 @@ namespace Umbraco.Core.Security
return manager;
}
- [EditorBrowsable(EditorBrowsableState.Never)]
- [Obsolete("Use the overload specifying all dependencies instead")]
- public static BackOfficeUserManager Create(
- IdentityFactoryOptions options,
- BackOfficeUserStore customUserStore,
- MembershipProviderBase membershipProvider)
- {
- var manager = new BackOfficeUserManager(customUserStore, options, membershipProvider);
- return manager;
- }
-
///
/// Creates a BackOfficeUserManager instance with all default options and a custom BackOfficeUserManager instance
///
@@ -114,16 +93,6 @@ namespace Umbraco.Core.Security
}
#endregion
- [EditorBrowsable(EditorBrowsableState.Never)]
- [Obsolete("Use the overload specifying all dependencies instead")]
- protected void InitUserManager(
- BackOfficeUserManager manager,
- MembershipProviderBase membershipProvider,
- IdentityFactoryOptions options)
- {
- InitUserManager(manager, membershipProvider, UmbracoConfig.For.UmbracoSettings().Content, options);
- }
-
///
/// Initializes the user manager with the correct options
///
@@ -154,7 +123,6 @@ namespace Umbraco.Core.Security
{
}
-
#region What we support do not currently
//TODO: We could support this - but a user claims will mostly just be what is in the auth cookie
@@ -183,17 +151,7 @@ namespace Umbraco.Core.Security
get { return false; }
}
#endregion
-
- [EditorBrowsable(EditorBrowsableState.Never)]
- [Obsolete("Use the overload specifying all dependencies instead")]
- protected void InitUserManager(
- BackOfficeUserManager manager,
- MembershipProviderBase membershipProvider,
- IDataProtectionProvider dataProtectionProvider)
- {
- InitUserManager(manager, membershipProvider, dataProtectionProvider, UmbracoConfig.For.UmbracoSettings().Content);
- }
-
+
///
/// Initializes the user manager with the correct options
///
diff --git a/src/Umbraco.Core/Security/OwinExtensions.cs b/src/Umbraco.Core/Security/OwinExtensions.cs
index b3bb5bffda..a4d596855c 100644
--- a/src/Umbraco.Core/Security/OwinExtensions.cs
+++ b/src/Umbraco.Core/Security/OwinExtensions.cs
@@ -2,6 +2,7 @@
using System.Web;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
+using Microsoft.Owin.Security;
using Umbraco.Core.Models.Identity;
namespace Umbraco.Core.Security
@@ -67,5 +68,7 @@ namespace Umbraco.Core.Security
return marker.GetManager(owinContext)
?? throw new NullReferenceException($"Could not resolve an instance of {typeof (BackOfficeUserManager)} from the {typeof (IOwinContext)}.");
}
+
}
+
}
diff --git a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs
index 605c8b4e9d..6cf5eacc97 100644
--- a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs
+++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs
@@ -7,6 +7,7 @@ using System.Security.Principal;
using System.Web;
using System.Web.Security;
using Microsoft.AspNet.Identity;
+using Microsoft.Owin.Security;
using Newtonsoft.Json;
using Umbraco.Core.Configuration;
@@ -21,10 +22,81 @@ namespace Umbraco.Core.Security
/// change over to 'pure' asp.net identity and just inherit from ClaimsIdentity.
///
[Serializable]
- public class UmbracoBackOfficeIdentity : FormsIdentity
+ public class UmbracoBackOfficeIdentity : ClaimsIdentity
{
public static UmbracoBackOfficeIdentity FromClaimsIdentity(ClaimsIdentity identity)
{
+ return new UmbracoBackOfficeIdentity(identity);
+ }
+
+ ///
+ /// Creates a new UmbracoBackOfficeIdentity
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public UmbracoBackOfficeIdentity(int userId, string username, string realName,
+ IEnumerable startContentNodes, IEnumerable startMediaNodes, string culture,
+ string sessionId, string securityStamp, IEnumerable allowedApps, IEnumerable roles)
+ : base(Enumerable.Empty(), Constants.Security.BackOfficeAuthenticationType) //this ctor is used to ensure the IsAuthenticated property is true
+ {
+ if (allowedApps == null) throw new ArgumentNullException(nameof(allowedApps));
+ if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
+ if (string.IsNullOrWhiteSpace(realName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName));
+ if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture));
+ if (string.IsNullOrWhiteSpace(sessionId)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(sessionId));
+ if (string.IsNullOrWhiteSpace(securityStamp)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp));
+ AddRequiredClaims(userId, username, realName, startContentNodes, startMediaNodes, culture, sessionId, securityStamp, allowedApps, roles);
+ }
+
+ ///
+ /// Creates a new UmbracoBackOfficeIdentity
+ ///
+ ///
+ /// The original identity created by the ClaimsIdentityFactory
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public UmbracoBackOfficeIdentity(ClaimsIdentity childIdentity,
+ int userId, string username, string realName,
+ IEnumerable startContentNodes, IEnumerable startMediaNodes, string culture,
+ string sessionId, string securityStamp, IEnumerable allowedApps, IEnumerable roles)
+ : base(childIdentity.Claims, Constants.Security.BackOfficeAuthenticationType)
+ {
+ if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
+ if (string.IsNullOrWhiteSpace(realName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName));
+ if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture));
+ if (string.IsNullOrWhiteSpace(sessionId)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(sessionId));
+ if (string.IsNullOrWhiteSpace(securityStamp)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp));
+ Actor = childIdentity;
+ AddRequiredClaims(userId, username, realName, startContentNodes, startMediaNodes, culture, sessionId, securityStamp, allowedApps, roles);
+ }
+
+ ///
+ /// Create a back office identity based on an existing claims identity
+ ///
+ ///
+ private UmbracoBackOfficeIdentity(ClaimsIdentity identity)
+ : base(identity.Claims, Constants.Security.BackOfficeAuthenticationType)
+ {
+ Actor = identity;
+
+ //validate that all claims exist
foreach (var t in RequiredBackOfficeIdentityClaimTypes)
{
//if the identity doesn't have the claim, or the claim value is null
@@ -33,145 +105,9 @@ namespace Umbraco.Core.Security
throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since the required claim " + t + " is missing");
}
}
-
- var username = identity.GetUserName();
- var session = identity.FindFirstValue(Constants.Security.SessionIdClaimType);
- var securityStamp = identity.FindFirstValue(Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType);
- var startContentId = identity.FindFirstValue(Constants.Security.StartContentNodeIdClaimType);
- var startMediaId = identity.FindFirstValue(Constants.Security.StartMediaNodeIdClaimType);
-
- var culture = identity.FindFirstValue(ClaimTypes.Locality);
- var id = identity.FindFirstValue(ClaimTypes.NameIdentifier);
- var realName = identity.FindFirstValue(ClaimTypes.GivenName);
-
- if (username == null || startContentId == null || startMediaId == null
- || culture == null || id == null
- || realName == null || session == null)
- throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since there are missing required claims");
-
- int[] startContentIdsAsInt;
- int[] startMediaIdsAsInt;
- if (startContentId.DetectIsJson() == false || startMediaId.DetectIsJson() == false)
- throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since the data is not formatted correctly - either content or media start Ids are not JSON");
-
- try
- {
- startContentIdsAsInt = JsonConvert.DeserializeObject(startContentId);
- startMediaIdsAsInt = JsonConvert.DeserializeObject(startMediaId);
- }
- catch (Exception e)
- {
- throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since the data is not formatted correctly - either content or media start Ids could not be parsed as JSON", e);
- }
-
- var roles = identity.FindAll(x => x.Type == DefaultRoleClaimType).Select(role => role.Value).ToList();
- var allowedApps = identity.FindAll(x => x.Type == Constants.Security.AllowedApplicationsClaimType).Select(app => app.Value).ToList();
-
- var userData = new UserData
- {
- SecurityStamp = securityStamp,
- SessionId = session,
- AllowedApplications = allowedApps.ToArray(),
- Culture = culture,
- Id = id,
- Roles = roles.ToArray(),
- Username = username,
- RealName = realName,
- StartContentNodes = startContentIdsAsInt,
- StartMediaNodes = startMediaIdsAsInt
- };
-
- return new UmbracoBackOfficeIdentity(identity, userData);
}
- ///
- /// Create a back office identity based on user data
- ///
- ///
- public UmbracoBackOfficeIdentity(UserData userdata)
- //This just creates a temp/fake ticket
- : base(new FormsAuthenticationTicket(userdata.Username, true, 10))
- {
- if (userdata == null) throw new ArgumentNullException("userdata");
- UserData = userdata;
- AddUserDataClaims();
- }
-
- ///
- /// Create a back office identity based on an existing claims identity
- ///
- ///
- ///
- public UmbracoBackOfficeIdentity(ClaimsIdentity claimsIdentity, UserData userdata)
- //This just creates a temp/fake ticket
- : base(new FormsAuthenticationTicket(userdata.Username, true, 10))
- {
- if (claimsIdentity == null) throw new ArgumentNullException("claimsIdentity");
- if (userdata == null) throw new ArgumentNullException("userdata");
-
- if (claimsIdentity is FormsIdentity)
- {
- //since it's a forms auth ticket, it is from a cookie so add that claim
- AddClaim(new Claim(ClaimTypes.CookiePath, "/", ClaimValueTypes.String, Issuer, Issuer, this));
- }
-
- _currentIssuer = claimsIdentity.AuthenticationType;
- UserData = userdata;
- AddExistingClaims(claimsIdentity);
- Actor = claimsIdentity;
- AddUserDataClaims();
- }
-
- ///
- /// Create a new identity from a forms auth ticket
- ///
- ///
- public UmbracoBackOfficeIdentity(FormsAuthenticationTicket ticket)
- : base(ticket)
- {
- //since it's a forms auth ticket, it is from a cookie so add that claim
- AddClaim(new Claim(ClaimTypes.CookiePath, "/", ClaimValueTypes.String, Issuer, Issuer, this));
-
- UserData = JsonConvert.DeserializeObject(ticket.UserData);
- AddUserDataClaims();
- }
-
- ///
- /// Used for cloning
- ///
- ///
- private UmbracoBackOfficeIdentity(UmbracoBackOfficeIdentity identity)
- : base(identity)
- {
- if (identity.Actor != null)
- {
- _currentIssuer = identity.AuthenticationType;
- AddExistingClaims(identity);
- Actor = identity.Clone();
- }
-
- UserData = identity.UserData;
- AddUserDataClaims();
- }
-
- public const string Issuer = "UmbracoBackOffice";
- private readonly string _currentIssuer = Issuer;
-
- ///
- /// Used during ctor to add existing claims from an existing ClaimsIdentity
- ///
- ///
- private void AddExistingClaims(ClaimsIdentity claimsIdentity)
- {
- foreach (var claim in claimsIdentity.Claims)
- {
- //In one special case we will replace a claim if it exists already and that is the
- // Forms auth claim for name which automatically gets added
- TryRemoveClaim(FindFirst(x => x.Type == claim.Type && x.Issuer == "Forms"));
-
- AddClaim(claim);
- }
- }
+ public const string Issuer = Constants.Security.BackOfficeAuthenticationType;
///
/// Returns the required claim types for a back office identity
@@ -179,153 +115,125 @@ namespace Umbraco.Core.Security
///
/// This does not incude the role claim type or allowed apps type since that is a collection and in theory could be empty
///
- public static IEnumerable RequiredBackOfficeIdentityClaimTypes
+ public static IEnumerable RequiredBackOfficeIdentityClaimTypes => new[]
{
- get
- {
- return new[]
- {
- ClaimTypes.NameIdentifier, //id
- ClaimTypes.Name, //username
- ClaimTypes.GivenName,
- Constants.Security.StartContentNodeIdClaimType,
- Constants.Security.StartMediaNodeIdClaimType,
- ClaimTypes.Locality,
- Constants.Security.SessionIdClaimType,
- Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType
- };
- }
- }
+ ClaimTypes.NameIdentifier, //id
+ ClaimTypes.Name, //username
+ ClaimTypes.GivenName,
+ Constants.Security.StartContentNodeIdClaimType,
+ Constants.Security.StartMediaNodeIdClaimType,
+ ClaimTypes.Locality,
+ Constants.Security.SessionIdClaimType,
+ Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType
+ };
///
- /// Adds claims based on the UserData data
+ /// Adds claims based on the ctor data
///
- private void AddUserDataClaims()
+ private void AddRequiredClaims(int userId, string username, string realName,
+ IEnumerable startContentNodes, IEnumerable startMediaNodes, string culture,
+ string sessionId, string securityStamp, IEnumerable allowedApps, IEnumerable roles)
{
//This is the id that 'identity' uses to check for the user id
if (HasClaim(x => x.Type == ClaimTypes.NameIdentifier) == false)
- AddClaim(new Claim(ClaimTypes.NameIdentifier, UserData.Id.ToString(), ClaimValueTypes.Integer32, Issuer, Issuer, this));
+ AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this));
if (HasClaim(x => x.Type == ClaimTypes.Name) == false)
- AddClaim(new Claim(ClaimTypes.Name, UserData.Username, ClaimValueTypes.String, Issuer, Issuer, this));
+ AddClaim(new Claim(ClaimTypes.Name, username, ClaimValueTypes.String, Issuer, Issuer, this));
if (HasClaim(x => x.Type == ClaimTypes.GivenName) == false)
- AddClaim(new Claim(ClaimTypes.GivenName, UserData.RealName, ClaimValueTypes.String, Issuer, Issuer, this));
+ AddClaim(new Claim(ClaimTypes.GivenName, realName, ClaimValueTypes.String, Issuer, Issuer, this));
- if (HasClaim(x => x.Type == Constants.Security.StartContentNodeIdClaimType) == false)
- AddClaim(new Claim(Constants.Security.StartContentNodeIdClaimType, JsonConvert.SerializeObject(StartContentNodes), ClaimValueTypes.Integer32, Issuer, Issuer, this));
+ if (HasClaim(x => x.Type == Constants.Security.StartContentNodeIdClaimType) == false && startContentNodes != null)
+ {
+ foreach (var startContentNode in startContentNodes)
+ {
+ AddClaim(new Claim(Constants.Security.StartContentNodeIdClaimType, startContentNode.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this));
+ }
+ }
- if (HasClaim(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) == false)
- AddClaim(new Claim(Constants.Security.StartMediaNodeIdClaimType, JsonConvert.SerializeObject(StartMediaNodes), ClaimValueTypes.Integer32, Issuer, Issuer, this));
+ if (HasClaim(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) == false && startMediaNodes != null)
+ {
+ foreach (var startMediaNode in startMediaNodes)
+ {
+ AddClaim(new Claim(Constants.Security.StartMediaNodeIdClaimType, startMediaNode.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this));
+ }
+ }
if (HasClaim(x => x.Type == ClaimTypes.Locality) == false)
- AddClaim(new Claim(ClaimTypes.Locality, Culture, ClaimValueTypes.String, Issuer, Issuer, this));
+ AddClaim(new Claim(ClaimTypes.Locality, culture, ClaimValueTypes.String, Issuer, Issuer, this));
if (HasClaim(x => x.Type == Constants.Security.SessionIdClaimType) == false && SessionId.IsNullOrWhiteSpace() == false)
- AddClaim(new Claim(Constants.Security.SessionIdClaimType, SessionId, ClaimValueTypes.String, Issuer, Issuer, this));
+ AddClaim(new Claim(Constants.Security.SessionIdClaimType, sessionId, ClaimValueTypes.String, Issuer, Issuer, this));
//The security stamp claim is also required... this is because this claim type is hard coded
// by the SecurityStampValidator, see: https://katanaproject.codeplex.com/workitem/444
if (HasClaim(x => x.Type == Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType) == false)
- AddClaim(new Claim(Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType, SecurityStamp, ClaimValueTypes.String, Issuer, Issuer, this));
+ AddClaim(new Claim(Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType, securityStamp, ClaimValueTypes.String, Issuer, Issuer, this));
//Add each app as a separate claim
- if (HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false)
+ if (HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false && allowedApps != null)
{
- foreach (var application in AllowedApplications)
+ foreach (var application in allowedApps)
{
AddClaim(new Claim(Constants.Security.AllowedApplicationsClaimType, application, ClaimValueTypes.String, Issuer, Issuer, this));
}
}
//Claims are added by the ClaimsIdentityFactory because our UserStore supports roles, however this identity might
- // not be made with that factory if it was created with a FormsAuthentication ticket so perform the check
- if (HasClaim(x => x.Type == DefaultRoleClaimType) == false)
+ // not be made with that factory if it was created with a different ticket so perform the check
+ if (HasClaim(x => x.Type == DefaultRoleClaimType) == false && roles != null)
{
- //manually add them based on the UserData
- foreach (var roleName in UserData.Roles)
+ //manually add them
+ foreach (var roleName in roles)
{
AddClaim(new Claim(RoleClaimType, roleName, ClaimValueTypes.String, Issuer, Issuer, this));
}
}
-
-
}
-
- protected internal UserData UserData { get; private set; }
-
+
+ ///
///
/// Gets the type of authenticated identity.
///
///
/// The type of authenticated identity. This property always returns "UmbracoBackOffice".
///
- public override string AuthenticationType
- {
- get { return _currentIssuer; }
- }
+ public override string AuthenticationType => Issuer;
- public int[] StartContentNodes
- {
- get { return UserData.StartContentNodes; }
- }
+ private int[] _startContentNodes;
+ public int[] StartContentNodes => _startContentNodes ?? (_startContentNodes = FindAll(x => x.Type == Constants.Security.StartContentNodeIdClaimType).Select(app => int.TryParse(app.Value, out var i) ? i : default).Where(x => x != default).ToArray());
- public int[] StartMediaNodes
- {
- get { return UserData.StartMediaNodes; }
- }
+ private int[] _startMediaNodes;
+ public int[] StartMediaNodes => _startMediaNodes ?? (_startMediaNodes = FindAll(x => x.Type == Constants.Security.StartMediaNodeIdClaimType).Select(app => int.TryParse(app.Value, out var i) ? i : default).Where(x => x != default).ToArray());
- public string[] AllowedApplications
- {
- get { return UserData.AllowedApplications; }
- }
+ private string[] _allowedApplications;
+ public string[] AllowedApplications => _allowedApplications ?? (_allowedApplications = FindAll(x => x.Type == Constants.Security.AllowedApplicationsClaimType).Select(app => app.Value).ToArray());
- public object Id
- {
- get { return UserData.Id; }
- }
+ public int Id => int.Parse(this.FindFirstValue(ClaimTypes.NameIdentifier));
- public string RealName
- {
- get { return UserData.RealName; }
- }
+ public string RealName => this.FindFirstValue(ClaimTypes.GivenName);
- public string Username
- {
- get { return UserData.Username; }
- }
+ public string Username => this.GetUserName();
- public string Culture
- {
- get { return UserData.Culture; }
- }
+ public string Culture => this.FindFirstValue(ClaimTypes.Locality);
public string SessionId
{
- get { return UserData.SessionId; }
+ get => this.FindFirstValue(Constants.Security.SessionIdClaimType);
+ set
+ {
+ var existing = FindFirst(Constants.Security.SessionIdClaimType);
+ if (existing != null)
+ TryRemoveClaim(existing);
+ AddClaim(new Claim(Constants.Security.SessionIdClaimType, value, ClaimValueTypes.String, Issuer, Issuer, this));
+ }
}
- public string SecurityStamp
- {
- get { return UserData.SecurityStamp; }
- }
-
- public string[] Roles
- {
- get { return UserData.Roles; }
- }
-
- ///
- /// Gets a copy of the current instance.
- ///
- ///
- /// A copy of the current instance.
- ///
- public override ClaimsIdentity Clone()
- {
- return new UmbracoBackOfficeIdentity(this);
- }
+ public string SecurityStamp => this.FindFirstValue(Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType);
+ public string[] Roles => this.FindAll(x => x.Type == DefaultRoleClaimType).Select(role => role.Value).ToArray();
+
}
}
diff --git a/src/Umbraco.Core/Security/UserData.cs b/src/Umbraco.Core/Security/UserData.cs
deleted file mode 100644
index 7e510ba708..0000000000
--- a/src/Umbraco.Core/Security/UserData.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-using System;
-using System.Runtime.Serialization;
-
-namespace Umbraco.Core.Security
-{
- ///
- /// Data structure used to store information in the authentication cookie
- ///
- [DataContract(Name = "userData", Namespace = "")]
- [Serializable]
- public class UserData
- {
- public UserData()
- {
- AllowedApplications = new string[] {};
- Roles = new string[] {};
- }
-
- ///
- /// Use this constructor to create/assign new UserData to the ticket
- ///
- ///
- /// The current sessionId for the user
- ///
- public UserData(string sessionId)
- {
- SessionId = sessionId;
- AllowedApplications = new string[] { };
- Roles = new string[] { };
- }
-
- ///
- /// Gets or sets the session identifier.
- ///
- [DataMember(Name = "sessionId")]
- public string SessionId { get; set; }
-
- ///
- /// Gets or sets the security stamp.
- ///
- [DataMember(Name = "securityStamp")]
- public string SecurityStamp { get; set; }
-
- [DataMember(Name = "id")]
- public object Id { get; set; }
-
- [DataMember(Name = "roles")]
- public string[] Roles { get; set; }
-
- [DataMember(Name = "username")]
- public string Username { get; set; }
-
- [DataMember(Name = "name")]
- public string RealName { get; set; }
-
- ///
- /// The start nodes on the UserData object for the auth ticket contains all of the user's start nodes including ones assigned to their user groups
- ///
- [DataMember(Name = "startContent")]
- public int[] StartContentNodes { get; set; }
-
- ///
- /// The start nodes on the UserData object for the auth ticket contains all of the user's start nodes including ones assigned to their user groups
- ///
- [DataMember(Name = "startMedia")]
- public int[] StartMediaNodes { get; set; }
-
- [DataMember(Name = "allowedApps")]
- public string[] AllowedApplications { get; set; }
-
- [DataMember(Name = "culture")]
- public string Culture { get; set; }
- }
-}
diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj
index cf1818f9e3..7790b8a769 100644
--- a/src/Umbraco.Core/Umbraco.Core.csproj
+++ b/src/Umbraco.Core/Umbraco.Core.csproj
@@ -1330,7 +1330,6 @@
-
diff --git a/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs b/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs
index f71c73d26e..acb53a895a 100644
--- a/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs
+++ b/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs
@@ -30,8 +30,9 @@ namespace Umbraco.Tests.Security
//This is the id that 'identity' uses to check for the username
new Claim(ClaimTypes.Name, "testing", ClaimValueTypes.String, TestIssuer, TestIssuer),
new Claim(ClaimTypes.GivenName, "hello world", ClaimValueTypes.String, TestIssuer, TestIssuer),
- new Claim(Constants.Security.StartContentNodeIdClaimType, "[-1]", ClaimValueTypes.Integer32, TestIssuer, TestIssuer),
- new Claim(Constants.Security.StartMediaNodeIdClaimType, "[5543]", ClaimValueTypes.Integer32, TestIssuer, TestIssuer),
+ new Claim(Constants.Security.StartContentNodeIdClaimType, "-1", ClaimValueTypes.Integer32, TestIssuer, TestIssuer),
+ new Claim(Constants.Security.StartMediaNodeIdClaimType, "5543", ClaimValueTypes.Integer32, TestIssuer, TestIssuer),
+ new Claim(Constants.Security.StartMediaNodeIdClaimType, "5555", ClaimValueTypes.Integer32, TestIssuer, TestIssuer),
new Claim(Constants.Security.AllowedApplicationsClaimType, "content", ClaimValueTypes.String, TestIssuer, TestIssuer),
new Claim(Constants.Security.AllowedApplicationsClaimType, "media", ClaimValueTypes.String, TestIssuer, TestIssuer),
new Claim(ClaimTypes.Locality, "en-us", ClaimValueTypes.String, TestIssuer, TestIssuer),
@@ -42,18 +43,18 @@ namespace Umbraco.Tests.Security
var backofficeIdentity = UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity);
- Assert.AreEqual("1234", backofficeIdentity.Id);
+ Assert.AreEqual(1234, backofficeIdentity.Id);
Assert.AreEqual(sessionId, backofficeIdentity.SessionId);
Assert.AreEqual(securityStamp, backofficeIdentity.SecurityStamp);
Assert.AreEqual("testing", backofficeIdentity.Username);
Assert.AreEqual("hello world", backofficeIdentity.RealName);
Assert.AreEqual(1, backofficeIdentity.StartContentNodes.Length);
- Assert.IsTrue(backofficeIdentity.StartMediaNodes.UnsortedSequenceEqual(new[] { 5543 }));
+ Assert.IsTrue(backofficeIdentity.StartMediaNodes.UnsortedSequenceEqual(new[] { 5543, 5555 }));
Assert.IsTrue(new[] {"content", "media"}.SequenceEqual(backofficeIdentity.AllowedApplications));
Assert.AreEqual("en-us", backofficeIdentity.Culture);
Assert.IsTrue(new[] { "admin" }.SequenceEqual(backofficeIdentity.Roles));
- Assert.AreEqual(11, backofficeIdentity.Claims.Count());
+ Assert.AreEqual(12, backofficeIdentity.Claims.Count());
}
[Test]
@@ -90,102 +91,36 @@ namespace Umbraco.Tests.Security
Assert.Throws(() => UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity));
}
- [Test]
- public void Create_With_User_Data()
- {
- var sessionId = Guid.NewGuid().ToString();
- var userData = new UserData(sessionId)
- {
- SecurityStamp = sessionId,
- AllowedApplications = new[] {"content", "media"},
- Culture = "en-us",
- Id = 1234,
- RealName = "hello world",
- Roles = new[] {"admin"},
- StartMediaNodes = new[] { 654 },
- Username = "testing"
- };
-
- var identity = new UmbracoBackOfficeIdentity(userData);
-
- Assert.AreEqual(11, identity.Claims.Count());
- }
[Test]
public void Create_With_Claims_And_User_Data()
{
var sessionId = Guid.NewGuid().ToString();
- var userData = new UserData(sessionId)
- {
- SecurityStamp = sessionId,
- AllowedApplications = new[] { "content", "media" },
- Culture = "en-us",
- Id = 1234,
- RealName = "hello world",
- Roles = new[] { "admin" },
- StartMediaNodes = new[] { 654 },
- Username = "testing"
- };
-
+
var claimsIdentity = new ClaimsIdentity(new[]
{
new Claim("TestClaim1", "test", ClaimValueTypes.Integer32, TestIssuer, TestIssuer),
new Claim("TestClaim1", "test", ClaimValueTypes.Integer32, TestIssuer, TestIssuer)
});
- var backofficeIdentity = new UmbracoBackOfficeIdentity(claimsIdentity, userData);
-
- Assert.AreEqual(13, backofficeIdentity.Claims.Count());
- }
-
- [Test]
- public void Create_With_Forms_Ticket()
- {
- var sessionId = Guid.NewGuid().ToString();
- var userData = new UserData(sessionId)
- {
- SecurityStamp = sessionId,
- AllowedApplications = new[] { "content", "media" },
- Culture = "en-us",
- Id = 1234,
- RealName = "hello world",
- Roles = new[] { "admin" },
- StartMediaNodes = new[] { 654 },
- Username = "testing"
- };
-
- var ticket = new FormsAuthenticationTicket(1, userData.Username, DateTime.Now, DateTime.Now.AddDays(1), true,
- JsonConvert.SerializeObject(userData));
-
- var identity = new UmbracoBackOfficeIdentity(ticket);
+ var identity = new UmbracoBackOfficeIdentity(claimsIdentity,
+ 1234, "testing", "hello world", new[] { 654 }, new[] { 654 }, "en-us", sessionId, sessionId, new[] { "content", "media" }, new[] { "admin" });
Assert.AreEqual(12, identity.Claims.Count());
}
+
[Test]
public void Clone()
{
var sessionId = Guid.NewGuid().ToString();
- var userData = new UserData(sessionId)
- {
- SecurityStamp = sessionId,
- AllowedApplications = new[] { "content", "media" },
- Culture = "en-us",
- Id = 1234,
- RealName = "hello world",
- Roles = new[] { "admin" },
- StartMediaNodes = new[] { 654 },
- Username = "testing"
- };
- var ticket = new FormsAuthenticationTicket(1, userData.Username, DateTime.Now, DateTime.Now.AddDays(1), true,
- JsonConvert.SerializeObject(userData));
-
- var identity = new UmbracoBackOfficeIdentity(ticket);
+ var identity = new UmbracoBackOfficeIdentity(
+ 1234, "testing", "hello world", new[] { 654 }, new[] { 654 }, "en-us", sessionId, sessionId, new[] { "content", "media" }, new[] { "admin" });
var cloned = identity.Clone();
- Assert.AreEqual(12, cloned.Claims.Count());
+ Assert.AreEqual(10, cloned.Claims.Count());
}
}
diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs
index c456475acc..ab473fd0c0 100644
--- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs
+++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/AuthenticateEverythingMiddleware.cs
@@ -26,16 +26,7 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting
{
var sessionId = Guid.NewGuid().ToString();
var identity = new UmbracoBackOfficeIdentity(
- new UserData(sessionId)
- {
- SecurityStamp = sessionId,
- Id = 0,
- Roles = new[] { "admin" },
- AllowedApplications = new[] { "content", "media", "members" },
- Culture = "en-US",
- RealName = "Admin",
- Username = "admin"
- });
+ -1, "admin", "Admin", null, null, "en-US", sessionId, sessionId, new[] { "content", "media", "members" }, new[] { "admin" });
return Task.FromResult(new AuthenticationTicket(identity,
new AuthenticationProperties()
diff --git a/src/Umbraco.Web/Editors/CurrentUserController.cs b/src/Umbraco.Web/Editors/CurrentUserController.cs
index a68ad4c813..a7d858c322 100644
--- a/src/Umbraco.Web/Editors/CurrentUserController.cs
+++ b/src/Umbraco.Web/Editors/CurrentUserController.cs
@@ -15,6 +15,7 @@ using System.Linq;
using Newtonsoft.Json;
using Umbraco.Core;
using Umbraco.Core.Security;
+using Umbraco.Web.Security;
using Umbraco.Web.WebApi.Filters;
diff --git a/src/Umbraco.Web/Security/AuthenticationExtensions.cs b/src/Umbraco.Web/Security/AuthenticationExtensions.cs
new file mode 100644
index 0000000000..0eeb840496
--- /dev/null
+++ b/src/Umbraco.Web/Security/AuthenticationExtensions.cs
@@ -0,0 +1,378 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Security.Claims;
+using System.Security.Principal;
+using System.Threading;
+using System.Web;
+using Microsoft.AspNet.Identity;
+using Microsoft.Owin;
+using Microsoft.Owin.Security;
+using Newtonsoft.Json;
+using Umbraco.Core;
+using Umbraco.Core.Composing;
+using Umbraco.Core.Configuration;
+using Umbraco.Core.Security;
+using Constants = Umbraco.Core.Constants;
+
+namespace Umbraco.Web.Security
+{
+ ///
+ /// Extensions to create and renew and remove authentication tickets for the Umbraco back office
+ ///
+ public static class AuthenticationExtensions
+ {
+ ///
+ /// This will check the ticket to see if it is valid, if it is it will set the current thread's user and culture
+ ///
+ ///
+ ///
+ /// If true will attempt to renew the ticket
+ public static bool AuthenticateCurrentRequest(this HttpContextBase http, AuthenticationTicket ticket, bool renewTicket)
+ {
+ if (http == null) throw new ArgumentNullException(nameof(http));
+
+ //if there was a ticket, it's not expired, - it should not be renewed or its renewable
+ if (ticket?.Properties.ExpiresUtc != null && ticket.Properties.ExpiresUtc.Value > DateTimeOffset.UtcNow && (renewTicket == false || http.RenewUmbracoAuthTicket()))
+ {
+ try
+ {
+ //get the Umbraco user identity
+ if (!(ticket.Identity is UmbracoBackOfficeIdentity identity))
+ throw new InvalidOperationException("The AuthenticationTicket specified does not contain the correct Identity type");
+
+ //set the principal object
+ var principal = new ClaimsPrincipal(identity);
+
+ //It is actually not good enough to set this on the current app Context and the thread, it also needs
+ // to be set explicitly on the HttpContext.Current !! This is a strange web api thing that is actually
+ // an underlying fault of asp.net not propogating the User correctly.
+ if (HttpContext.Current != null)
+ {
+ HttpContext.Current.User = principal;
+ }
+ http.User = principal;
+ Thread.CurrentPrincipal = principal;
+
+ //This is a back office request, we will also set the culture/ui culture
+ Thread.CurrentThread.CurrentCulture =
+ Thread.CurrentThread.CurrentUICulture =
+ new System.Globalization.CultureInfo(identity.Culture);
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ if (ex is FormatException || ex is JsonReaderException)
+ {
+ //this will occur if the cookie data is invalid
+ http.UmbracoLogout();
+ }
+ else
+ {
+ throw;
+ }
+
+ }
+ }
+
+ return false;
+ }
+
+
+ ///
+ /// This will return the current back office identity.
+ ///
+ ///
+ ///
+ /// If set to true and a back office identity is not found and not authenticated, this will attempt to authenticate the
+ /// request just as is done in the Umbraco module and then set the current identity if it is valid.
+ /// Just like in the UmbracoModule, if this is true then the user's culture will be assigned to the request.
+ ///
+ ///
+ /// Returns the current back office identity if an admin is authenticated otherwise null
+ ///
+ public static UmbracoBackOfficeIdentity GetCurrentIdentity(this HttpContextBase http, bool authenticateRequestIfNotFound)
+ {
+ if (http == null) throw new ArgumentNullException(nameof(http));
+ if (http.User == null) return null; //there's no user at all so no identity
+
+ //If it's already a UmbracoBackOfficeIdentity
+ var backOfficeIdentity = http.User.GetUmbracoIdentity();
+ if (backOfficeIdentity != null) return backOfficeIdentity;
+
+ if (authenticateRequestIfNotFound == false) return null;
+
+ //even if authenticateRequestIfNotFound is true we cannot continue if the request is actually authenticated
+ // which would mean something strange is going on that it is not an umbraco identity.
+ if (http.User.Identity.IsAuthenticated) return null;
+
+ //So the user is not authed but we've been asked to do the auth if authenticateRequestIfNotFound = true,
+ // which might occur in old webforms style things or for routes that aren't included as a back office request.
+ // in this case, we are just reverting to authing using the cookie.
+
+ // TODO: Even though this is in theory legacy, we have legacy bits laying around and we'd need to do the auth based on
+ // how the Module will eventually do it (by calling in to any registered authenticators).
+
+ var ticket = http.GetUmbracoAuthTicket();
+ if (http.AuthenticateCurrentRequest(ticket, true))
+ {
+ //now we 'should have an umbraco identity
+ return http.User.Identity as UmbracoBackOfficeIdentity;
+ }
+ return null;
+ }
+
+ ///
+ /// This will return the current back office identity.
+ ///
+ ///
+ ///
+ /// If set to true and a back office identity is not found and not authenticated, this will attempt to authenticate the
+ /// request just as is done in the Umbraco module and then set the current identity if it is valid
+ ///
+ ///
+ /// Returns the current back office identity if an admin is authenticated otherwise null
+ ///
+ internal static UmbracoBackOfficeIdentity GetCurrentIdentity(this HttpContext http, bool authenticateRequestIfNotFound)
+ {
+ if (http == null) throw new ArgumentNullException("http");
+ return new HttpContextWrapper(http).GetCurrentIdentity(authenticateRequestIfNotFound);
+ }
+
+ public static void UmbracoLogout(this HttpContextBase http)
+ {
+ if (http == null) throw new ArgumentNullException("http");
+ Logout(http, UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName);
+ }
+
+ ///
+ /// This clears the forms authentication cookie
+ ///
+ ///
+ internal static void UmbracoLogout(this HttpContext http)
+ {
+ if (http == null) throw new ArgumentNullException("http");
+ new HttpContextWrapper(http).UmbracoLogout();
+ }
+
+ ///
+ /// This will force ticket renewal in the OWIN pipeline
+ ///
+ ///
+ ///
+ public static bool RenewUmbracoAuthTicket(this HttpContextBase http)
+ {
+ if (http == null) throw new ArgumentNullException("http");
+ http.Items[Constants.Security.ForceReAuthFlag] = true;
+ return true;
+ }
+
+ ///
+ /// This will force ticket renewal in the OWIN pipeline
+ ///
+ ///
+ ///
+ internal static bool RenewUmbracoAuthTicket(this HttpContext http)
+ {
+ if (http == null) throw new ArgumentNullException("http");
+ http.Items[Constants.Security.ForceReAuthFlag] = true;
+ return true;
+ }
+
+ /////
+ ///// Creates the umbraco authentication ticket
+ /////
+ /////
+ /////
+ //public static AuthenticationTicket CreateUmbracoAuthTicket(this HttpContextBase http, UserData userdata)
+ //{
+ // //ONLY used by BasePage.doLogin!
+
+ // if (http == null) throw new ArgumentNullException("http");
+ // if (userdata == null) throw new ArgumentNullException("userdata");
+
+ // var userDataString = JsonConvert.SerializeObject(userdata);
+ // return CreateAuthTicketAndCookie(
+ // http,
+ // userdata.Username,
+ // userDataString,
+ // //use the configuration timeout - this is the same timeout that will be used when renewing the ticket.
+ // GlobalSettings.TimeOutInMinutes,
+ // //Umbraco has always persisted it's original cookie for 1 day so we'll keep it that way
+ // 1440,
+ // UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName,
+ // UmbracoConfig.For.UmbracoSettings().Security.AuthCookieDomain);
+ //}
+
+ ///
+ /// returns the number of seconds the user has until their auth session times out
+ ///
+ ///
+ ///
+ public static double GetRemainingAuthSeconds(this HttpContextBase http)
+ {
+ if (http == null) throw new ArgumentNullException(nameof(http));
+ var ticket = http.GetUmbracoAuthTicket();
+ return ticket.GetRemainingAuthSeconds();
+ }
+
+ public static double GetRemainingAuthSeconds(this IOwinContext owinCtx)
+ {
+ if (owinCtx == null) throw new ArgumentNullException(nameof(owinCtx));
+ var ticket = owinCtx.GetUmbracoAuthTicket();
+ return ticket.GetRemainingAuthSeconds();
+ }
+
+ ///
+ /// returns the number of seconds the user has until their auth session times out
+ ///
+ ///
+ ///
+ public static double GetRemainingAuthSeconds(this AuthenticationTicket ticket)
+ {
+ var utcExpired = ticket?.Properties.ExpiresUtc;
+ if (utcExpired == null) return 0;
+ var secondsRemaining = utcExpired.Value.Subtract(DateTimeOffset.UtcNow).TotalSeconds;
+ return secondsRemaining;
+ }
+
+ ///
+ /// Gets the umbraco auth ticket
+ ///
+ ///
+ ///
+ public static AuthenticationTicket GetUmbracoAuthTicket(this HttpContextBase http)
+ {
+ if (http == null) throw new ArgumentNullException(nameof(http));
+ return GetAuthTicket(http, UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName);
+ }
+
+ internal static AuthenticationTicket GetUmbracoAuthTicket(this HttpContext http)
+ {
+ if (http == null) throw new ArgumentNullException(nameof(http));
+ return new HttpContextWrapper(http).GetUmbracoAuthTicket();
+ }
+
+ public static AuthenticationTicket GetUmbracoAuthTicket(this IOwinContext ctx)
+ {
+ if (ctx == null) throw new ArgumentNullException(nameof(ctx));
+ return GetAuthTicket(ctx, UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName);
+ }
+
+ ///
+ /// This clears the forms authentication cookie
+ ///
+ ///
+ ///
+ private static void Logout(this HttpContextBase http, string cookieName)
+ {
+ //We need to clear the sessionId from the database. This is legacy code to do any logging out and shouldn't really be used at all but in any case
+ //we need to make sure the session is cleared. Due to the legacy nature of this it means we need to use singletons
+ if (http.User != null)
+ {
+ var claimsIdentity = http.User.Identity as ClaimsIdentity;
+ if (claimsIdentity != null)
+ {
+ var sessionId = claimsIdentity.FindFirstValue(Constants.Security.SessionIdClaimType);
+ Guid guidSession;
+ if (sessionId.IsNullOrWhiteSpace() == false && Guid.TryParse(sessionId, out guidSession))
+ {
+ Current.Services.UserService.ClearLoginSession(guidSession);
+ }
+ }
+ }
+
+ if (http == null) throw new ArgumentNullException("http");
+ //clear the preview cookie and external login
+ var cookies = new[] { cookieName, Constants.Web.PreviewCookieName, Constants.Security.BackOfficeExternalCookieName };
+ foreach (var c in cookies)
+ {
+ //remove from the request
+ http.Request.Cookies.Remove(c);
+
+ //expire from the response
+ var formsCookie = http.Response.Cookies[c];
+ if (formsCookie != null)
+ {
+ //this will expire immediately and be removed from the browser
+ formsCookie.Expires = DateTime.Now.AddYears(-1);
+ }
+ else
+ {
+ //ensure there's def an expired cookie
+ http.Response.Cookies.Add(new HttpCookie(c) { Expires = DateTime.Now.AddYears(-1) });
+ }
+ }
+ }
+
+ private static AuthenticationTicket GetAuthTicket(this IOwinContext owinCtx, string cookieName)
+ {
+ var asDictionary = new Dictionary();
+ foreach (var requestCookie in owinCtx.Request.Cookies)
+ {
+ var key = requestCookie.Key;
+ asDictionary[key] = requestCookie.Value;
+ }
+
+ var secureFormat = owinCtx.GetUmbracoAuthTicketDataProtector();
+
+ //get the ticket
+ try
+ {
+
+ return GetAuthTicket(secureFormat, asDictionary, cookieName);
+ }
+ catch (Exception)
+ {
+ owinCtx.Authentication.SignOut(
+ Constants.Security.BackOfficeAuthenticationType,
+ Constants.Security.BackOfficeExternalAuthenticationType);
+ return null;
+ }
+ }
+
+ private static AuthenticationTicket GetAuthTicket(this HttpContextBase http, string cookieName)
+ {
+ var asDictionary = new Dictionary();
+ for (var i = 0; i < http.Request.Cookies.Keys.Count; i++)
+ {
+ var key = http.Request.Cookies.Keys.Get(i);
+ asDictionary[key] = http.Request.Cookies[key].Value;
+ }
+
+ var owinCtx = http.GetOwinContext();
+ var secureFormat = owinCtx.GetUmbracoAuthTicketDataProtector();
+
+ //get the ticket
+ try
+ {
+ return GetAuthTicket(secureFormat, asDictionary, cookieName);
+ }
+ catch (Exception)
+ {
+ //occurs when decryption fails
+ http.Logout(cookieName);
+ return null;
+ }
+ }
+
+ private static AuthenticationTicket GetAuthTicket(ISecureDataFormat secureDataFormat, IDictionary cookies, string cookieName)
+ {
+ if (cookies == null) throw new ArgumentNullException(nameof(cookies));
+
+ if (cookies.ContainsKey(cookieName) == false) return null;
+
+ var formsCookie = cookies[cookieName];
+ if (formsCookie == null)
+ {
+ return null;
+ }
+ //get the ticket
+
+ return secureDataFormat.Unprotect(formsCookie);
+ }
+ }
+}
diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs
index 02a7208634..260149be84 100644
--- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs
+++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs
@@ -8,6 +8,8 @@ using Microsoft.Owin.Extensions;
using Microsoft.Owin.Logging;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
+using Microsoft.Owin.Security.DataHandler;
+using Microsoft.Owin.Security.DataProtection;
using Owin;
using Umbraco.Core;
using Umbraco.Core.Configuration;
@@ -178,17 +180,21 @@ namespace Umbraco.Web.Security.Identity
//don't apply if app is not ready
if (runtimeState.Level != RuntimeLevel.Upgrade && runtimeState.Level != RuntimeLevel.Run) return app;
- var getSecondsOptions = app.CreateUmbracoCookieAuthOptions(
+ var cookieAuthOptions = app.CreateUmbracoCookieAuthOptions(
//This defines the explicit path read cookies from for this middleware
- new[] {string.Format("{0}/backoffice/UmbracoApi/Authentication/GetRemainingTimeoutSeconds", GlobalSettings.Path)});
- getSecondsOptions.Provider = cookieOptions.Provider;
+ new[] {$"{GlobalSettings.Path}/backoffice/UmbracoApi/Authentication/GetRemainingTimeoutSeconds"});
+ cookieAuthOptions.Provider = cookieOptions.Provider;
//This is a custom middleware, we need to return the user's remaining logged in seconds
app.Use(
- getSecondsOptions,
+ cookieAuthOptions,
UmbracoConfig.For.UmbracoSettings().Security,
app.CreateLogger());
+ //This is required so that we can read the auth ticket format outside of this pipeline
+ app.CreatePerOwinContext(
+ (options, context) => new UmbracoAuthTicketDataProtector(cookieOptions.TicketDataFormat));
+
return app;
}
@@ -346,11 +352,19 @@ namespace Umbraco.Web.Security.Identity
///
public static UmbracoBackOfficeCookieAuthOptions CreateUmbracoCookieAuthOptions(this IAppBuilder app, string[] explicitPaths = null)
{
+ //this is how aspnet wires up the default AuthenticationTicket protector so we'll use the same code
+ var ticketDataFormat = new TicketDataFormat(
+ app.CreateDataProtector(typeof (CookieAuthenticationMiddleware).FullName,
+ Constants.Security.BackOfficeAuthenticationType,
+ "v1"));
+
var authOptions = new UmbracoBackOfficeCookieAuthOptions(
explicitPaths,
UmbracoConfig.For.UmbracoSettings().Security,
GlobalSettings.TimeOutInMinutes,
- GlobalSettings.UseSSL);
+ GlobalSettings.UseSSL,
+ ticketDataFormat);
+
return authOptions;
}
}
diff --git a/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs b/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs
index 98493db1c7..43c84c2ba9 100644
--- a/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs
+++ b/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs
@@ -6,7 +6,7 @@ using Microsoft.Owin;
using Microsoft.Owin.Infrastructure;
using Umbraco.Core;
using Umbraco.Core.Configuration;
-using Umbraco.Core.IO;
+using Umbraco.Core.Security;
namespace Umbraco.Web.Security.Identity
{
diff --git a/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs b/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs
deleted file mode 100644
index 64da407021..0000000000
--- a/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-using System;
-using System.Security.Claims;
-using System.Web.Security;
-using Microsoft.Owin;
-using Microsoft.Owin.Security;
-using Microsoft.Owin.Security.Cookies;
-using Newtonsoft.Json;
-using Owin;
-using Umbraco.Core.Security;
-
-namespace Umbraco.Web.Security.Identity
-{
-
- ///
- /// Custom secure format that uses the old FormsAuthentication format
- ///
- internal class FormsAuthenticationSecureDataFormat : ISecureDataFormat
- {
- private readonly int _loginTimeoutMinutes;
-
- public FormsAuthenticationSecureDataFormat(int loginTimeoutMinutes)
- {
- _loginTimeoutMinutes = loginTimeoutMinutes;
- }
-
- public string Protect(AuthenticationTicket data)
- {
- var backofficeIdentity = (UmbracoBackOfficeIdentity)data.Identity;
- var userDataString = JsonConvert.SerializeObject(backofficeIdentity.UserData);
-
- var ticket = new FormsAuthenticationTicket(
- 5,
- data.Identity.Name,
- data.Properties.IssuedUtc.HasValue
- ? data.Properties.IssuedUtc.Value.LocalDateTime
- : DateTime.Now,
- data.Properties.ExpiresUtc.HasValue
- ? data.Properties.ExpiresUtc.Value.LocalDateTime
- : DateTime.Now.AddMinutes(_loginTimeoutMinutes),
- data.Properties.IsPersistent,
- userDataString,
- "/"
- );
-
- return FormsAuthentication.Encrypt(ticket);
- }
-
- ///
- /// Unprotects the cookie
- ///
- ///
- ///
- public AuthenticationTicket Unprotect(string protectedText)
- {
- FormsAuthenticationTicket decrypt;
- try
- {
- decrypt = FormsAuthentication.Decrypt(protectedText);
- if (decrypt == null) return null;
- }
- catch (Exception)
- {
- return null;
- }
-
- UmbracoBackOfficeIdentity identity;
-
- try
- {
- identity = new UmbracoBackOfficeIdentity(decrypt);
- }
- catch (Exception)
- {
- //if it cannot be created return null, will be due to serialization errors in user data most likely due to corrupt cookies or cookies
- //for previous versions of Umbraco
- return null;
- }
-
- var ticket = new AuthenticationTicket(identity, new AuthenticationProperties
- {
- ExpiresUtc = decrypt.Expiration.ToUniversalTime(),
- IssuedUtc = decrypt.IssueDate.ToUniversalTime(),
- IsPersistent = decrypt.IsPersistent,
- AllowRefresh = true
- });
-
- return ticket;
- }
- }
-}
diff --git a/src/Umbraco.Web/Security/Identity/GetUserSecondsMiddleWare.cs b/src/Umbraco.Web/Security/Identity/GetUserSecondsMiddleWare.cs
index 0efb0b66a6..075fe89cdc 100644
--- a/src/Umbraco.Web/Security/Identity/GetUserSecondsMiddleWare.cs
+++ b/src/Umbraco.Web/Security/Identity/GetUserSecondsMiddleWare.cs
@@ -35,11 +35,9 @@ namespace Umbraco.Web.Security.Identity
ILogger logger)
: base(next)
{
- if (authOptions == null) throw new ArgumentNullException("authOptions");
- if (logger == null) throw new ArgumentNullException("logger");
- _authOptions = authOptions;
+ _authOptions = authOptions ?? throw new ArgumentNullException(nameof(authOptions));
_security = security;
- _logger = logger;
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public override async Task Invoke(IOwinContext context)
@@ -49,7 +47,7 @@ namespace Umbraco.Web.Security.Identity
if (request.Uri.Scheme.InvariantStartsWith("http")
&& request.Uri.AbsolutePath.InvariantEquals(
- string.Format("{0}/backoffice/UmbracoApi/Authentication/GetRemainingTimeoutSeconds", GlobalSettings.Path)))
+ $"{GlobalSettings.Path}/backoffice/UmbracoApi/Authentication/GetRemainingTimeoutSeconds"))
{
var cookie = _authOptions.CookieManager.GetRequestCookie(context, _security.AuthCookieName);
if (cookie.IsNullOrWhiteSpace() == false)
diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs
index 714337fb2d..679e1c8b67 100644
--- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs
+++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs
@@ -14,29 +14,19 @@ namespace Umbraco.Web.Security.Identity
///
public sealed class UmbracoBackOfficeCookieAuthOptions : CookieAuthenticationOptions
{
- public int LoginTimeoutMinutes { get; private set; }
-
- public UmbracoBackOfficeCookieAuthOptions()
- : this(UmbracoConfig.For.UmbracoSettings().Security, GlobalSettings.TimeOutInMinutes, GlobalSettings.UseSSL)
- {
- }
-
+ public int LoginTimeoutMinutes { get; }
+
public UmbracoBackOfficeCookieAuthOptions(
string[] explicitPaths,
ISecuritySection securitySection,
int loginTimeoutMinutes,
bool forceSsl,
- bool useLegacyFormsAuthDataFormat = true)
+ ISecureDataFormat secureDataFormat)
{
+ var secureDataFormat1 = secureDataFormat ?? throw new ArgumentNullException(nameof(secureDataFormat));
LoginTimeoutMinutes = loginTimeoutMinutes;
AuthenticationType = Constants.Security.BackOfficeAuthenticationType;
-
- if (useLegacyFormsAuthDataFormat)
- {
- //If this is not explicitly set it will fall back to the default automatically
- TicketDataFormat = new FormsAuthenticationSecureDataFormat(LoginTimeoutMinutes);
- }
-
+
SlidingExpiration = true;
ExpireTimeSpan = TimeSpan.FromMinutes(LoginTimeoutMinutes);
CookieDomain = securitySection.AuthCookieDomain;
@@ -45,19 +35,12 @@ namespace Umbraco.Web.Security.Identity
CookieSecure = forceSsl ? CookieSecureOption.Always : CookieSecureOption.SameAsRequest;
CookiePath = "/";
+ TicketDataFormat = new UmbracoSecureDataFormat(LoginTimeoutMinutes, secureDataFormat1);
+
//Custom cookie manager so we can filter requests
CookieManager = new BackOfficeCookieManager(Current.UmbracoContextAccessor, Current.RuntimeState, explicitPaths);
}
-
- public UmbracoBackOfficeCookieAuthOptions(
- ISecuritySection securitySection,
- int loginTimeoutMinutes,
- bool forceSsl,
- bool useLegacyFormsAuthDataFormat = true)
- : this(null, securitySection, loginTimeoutMinutes, forceSsl, useLegacyFormsAuthDataFormat)
- {
- }
-
+
///
/// Creates the cookie options for saving the auth cookie
///
@@ -66,8 +49,8 @@ namespace Umbraco.Web.Security.Identity
///
public CookieOptions CreateRequestCookieOptions(IOwinContext ctx, AuthenticationTicket ticket)
{
- if (ctx == null) throw new ArgumentNullException("ctx");
- if (ticket == null) throw new ArgumentNullException("ticket");
+ if (ctx == null) throw new ArgumentNullException(nameof(ctx));
+ if (ticket == null) throw new ArgumentNullException(nameof(ticket));
var issuedUtc = ticket.Properties.IssuedUtc ?? SystemClock.UtcNow;
var expiresUtc = ticket.Properties.ExpiresUtc ?? issuedUtc.Add(ExpireTimeSpan);
diff --git a/src/Umbraco.Web/Security/Identity/UmbracoSecureDataFormat.cs b/src/Umbraco.Web/Security/Identity/UmbracoSecureDataFormat.cs
new file mode 100644
index 0000000000..2676a5ee25
--- /dev/null
+++ b/src/Umbraco.Web/Security/Identity/UmbracoSecureDataFormat.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Security.Claims;
+using System.Web.Security;
+using Microsoft.Owin;
+using Microsoft.Owin.Security;
+using Microsoft.Owin.Security.Cookies;
+using Newtonsoft.Json;
+using Owin;
+using Umbraco.Core.Security;
+
+namespace Umbraco.Web.Security.Identity
+{
+
+ ///
+ /// Custom secure format that ensures the Identity in the ticket is and not just a ClaimsIdentity
+ ///
+ internal class UmbracoSecureDataFormat : ISecureDataFormat
+ {
+ private readonly int _loginTimeoutMinutes;
+ private readonly ISecureDataFormat _ticketDataFormat;
+
+ public UmbracoSecureDataFormat(int loginTimeoutMinutes, ISecureDataFormat ticketDataFormat)
+ {
+ _loginTimeoutMinutes = loginTimeoutMinutes;
+ _ticketDataFormat = ticketDataFormat ?? throw new ArgumentNullException(nameof(ticketDataFormat));
+ }
+
+ public string Protect(AuthenticationTicket data)
+ {
+ var backofficeIdentity = (UmbracoBackOfficeIdentity)data.Identity;
+
+ //create a new ticket based on the passed in tickets details, however, we'll adjust the expires utc based on the specified timeout mins
+ var ticket = new AuthenticationTicket(backofficeIdentity,
+ new AuthenticationProperties(data.Properties.Dictionary)
+ {
+ IssuedUtc = data.Properties.IssuedUtc,
+ ExpiresUtc = data.Properties.ExpiresUtc ?? DateTimeOffset.UtcNow.AddMinutes(_loginTimeoutMinutes),
+ AllowRefresh = data.Properties.AllowRefresh,
+ IsPersistent = data.Properties.IsPersistent,
+ RedirectUri = data.Properties.RedirectUri
+ });
+
+ return _ticketDataFormat.Protect(ticket);
+ }
+
+ ///
+ /// Unprotects the cookie
+ ///
+ ///
+ ///
+ public AuthenticationTicket Unprotect(string protectedText)
+ {
+ AuthenticationTicket decrypt;
+ try
+ {
+ decrypt = _ticketDataFormat.Unprotect(protectedText);
+ if (decrypt == null) return null;
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+
+ UmbracoBackOfficeIdentity identity;
+
+ try
+ {
+ identity = UmbracoBackOfficeIdentity.FromClaimsIdentity(decrypt.Identity);
+ }
+ catch (Exception)
+ {
+ //if it cannot be created return null, will be due to serialization errors in user data most likely due to corrupt cookies or cookies
+ //for previous versions of Umbraco
+ return null;
+ }
+
+ //return the ticket with a UmbracoBackOfficeIdentity
+ var ticket = new AuthenticationTicket(identity, decrypt.Properties);
+
+ return ticket;
+ }
+ }
+}
diff --git a/src/Umbraco.Web/Security/LegacyDefaultAppMapping.cs b/src/Umbraco.Web/Security/LegacyDefaultAppMapping.cs
deleted file mode 100644
index 54ed9aea33..0000000000
--- a/src/Umbraco.Web/Security/LegacyDefaultAppMapping.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
-namespace Umbraco.Web.Security
-{
- ///
- /// This is used specifically to assign a default 'app' to a particular section in order to validate the
- /// currently logged in user's allowed applications
- ///
- ///
- /// This relates to these issues:
- /// http://issues.umbraco.org/issue/U4-2021
- /// http://issues.umbraco.org/issue/U4-529
- ///
- /// In order to fix these issues we need to pass in an 'app' parameter but since we don't want to break compatibility
- /// we will create this mapping to map a 'default application' to a section action (like creating or deleting)
- ///
- public static class LegacyDefaultAppMapping
- {
- ///
- /// Constructor that assigns all initial known mappings
- ///
- static LegacyDefaultAppMapping()
- {
- }
-
- private static readonly ConcurrentDictionary NodeTypeAliasMapping = new ConcurrentDictionary();
-
- ///
- /// Adds the default app mapping to the node type
- ///
- /// The nodeType is the same nodeType found in the UI.xml
- /// The default app associated with this nodeType if the 'app' parameter was not detected
- public static void AddNodeTypeMappingForCreateDialog(string nodeType, string defaultApp)
- {
- NodeTypeAliasMapping.AddOrUpdate(nodeType, s => defaultApp, (s, s1) => defaultApp);
- }
-
- internal static string GetDefaultAppForCreateDialog(string nodeTypeAlias)
- {
- string app;
- return NodeTypeAliasMapping.TryGetValue(nodeTypeAlias, out app) ? app : null;
- }
-
- }
-}
diff --git a/src/Umbraco.Web/Security/OwinExtensions.cs b/src/Umbraco.Web/Security/OwinExtensions.cs
index 0df0c28cf6..c9d3c56513 100644
--- a/src/Umbraco.Web/Security/OwinExtensions.cs
+++ b/src/Umbraco.Web/Security/OwinExtensions.cs
@@ -1,7 +1,7 @@
-using System;
-using System.Web;
+using System.Web;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
+using Microsoft.Owin.Security;
using Umbraco.Core;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Security;
@@ -11,16 +11,14 @@ namespace Umbraco.Web.Security
{
internal static class OwinExtensions
{
-
///
- /// Nasty little hack to get httpcontextbase from an owin context
+ /// Gets the for the Umbraco back office cookie
///
///
///
- internal static Attempt TryGetHttpContext(this IOwinContext owinContext)
+ internal static ISecureDataFormat GetUmbracoAuthTicketDataProtector(this IOwinContext owinContext)
{
- var ctx = owinContext.Get(typeof(HttpContextBase).FullName);
- return ctx == null ? Attempt.Fail() : Attempt.Succeed(ctx);
+ return owinContext.Get().Protector;
}
}
diff --git a/src/Umbraco.Web/Security/UmbracoAuthTicketDataProtector.cs b/src/Umbraco.Web/Security/UmbracoAuthTicketDataProtector.cs
new file mode 100644
index 0000000000..c65c010204
--- /dev/null
+++ b/src/Umbraco.Web/Security/UmbracoAuthTicketDataProtector.cs
@@ -0,0 +1,19 @@
+using System;
+using Microsoft.Owin.Security;
+using Umbraco.Core;
+
+namespace Umbraco.Web.Security
+{
+ ///
+ /// This is used so that we can retrive the auth ticket protector from an IOwinContext
+ ///
+ internal class UmbracoAuthTicketDataProtector : DisposableObjectSlim
+ {
+ public UmbracoAuthTicketDataProtector(ISecureDataFormat protector)
+ {
+ Protector = protector ?? throw new ArgumentNullException(nameof(protector));
+ }
+
+ public ISecureDataFormat Protector { get; }
+ }
+}
diff --git a/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs b/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs
index 5fcc730f90..3c100a839e 100644
--- a/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs
+++ b/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs
@@ -11,6 +11,7 @@ using Umbraco.Core.Models;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Security;
using Umbraco.Web.Composing;
+using Umbraco.Web.Security;
using Umbraco.Web._Legacy.Actions;
namespace Umbraco.Web.UI.Pages
diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj
index 9a8d424708..35c669ebd7 100644
--- a/src/Umbraco.Web/Umbraco.Web.csproj
+++ b/src/Umbraco.Web/Umbraco.Web.csproj
@@ -404,6 +404,9 @@
+
+
+
@@ -769,7 +772,6 @@
-
@@ -1084,7 +1086,6 @@
-
diff --git a/src/Umbraco.Web/WebApi/Filters/UmbracoUserTimeoutFilterAttribute.cs b/src/Umbraco.Web/WebApi/Filters/UmbracoUserTimeoutFilterAttribute.cs
index 2d938eb6e8..0d54ed99eb 100644
--- a/src/Umbraco.Web/WebApi/Filters/UmbracoUserTimeoutFilterAttribute.cs
+++ b/src/Umbraco.Web/WebApi/Filters/UmbracoUserTimeoutFilterAttribute.cs
@@ -1,6 +1,8 @@
-using System.Globalization;
+using System;
+using System.Globalization;
using System.Web.Http.Filters;
using Umbraco.Core.Security;
+using Umbraco.Web.Security;
namespace Umbraco.Web.WebApi.Filters
{
@@ -17,12 +19,13 @@ namespace Umbraco.Web.WebApi.Filters
//this can occur if an error has already occurred.
if (actionExecutedContext.Response == null) return;
-
+
var httpContextAttempt = actionExecutedContext.Request.TryGetHttpContext();
if (httpContextAttempt.Success)
- {
+ {
+
var ticket = httpContextAttempt.Result.GetUmbracoAuthTicket();
- if (ticket != null && ticket.Expired == false)
+ if (ticket?.Properties.ExpiresUtc != null && ticket.Properties.ExpiresUtc.Value < DateTimeOffset.UtcNow)
{
var remainingSeconds = httpContextAttempt.Result.GetRemainingAuthSeconds();
actionExecutedContext.Response.Headers.Add("X-Umb-User-Seconds", remainingSeconds.ToString(CultureInfo.InvariantCulture));
diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Tree/CustomTreeService.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Tree/CustomTreeService.cs
index 70b08fbf0f..f3143bad30 100644
--- a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Tree/CustomTreeService.cs
+++ b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Tree/CustomTreeService.cs
@@ -11,6 +11,7 @@ using umbraco.cms.businesslogic;
using umbraco.cms.presentation.Trees;
using umbraco.controls.Tree;
using Umbraco.Core.Services;
+using Umbraco.Web.Security;
using Umbraco.Web.WebServices;
namespace umbraco.controls.Tree
diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Tree/TreeControl.ascx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Tree/TreeControl.ascx.cs
index 385c4c1843..67d82f327f 100644
--- a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Tree/TreeControl.ascx.cs
+++ b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Tree/TreeControl.ascx.cs
@@ -13,6 +13,7 @@ using System.Drawing;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Services;
+using Umbraco.Web.Security;
namespace umbraco.controls.Tree
{