Removes FormsAuthentication cookie format and replaces with standard aspnet identity format, removes a bunch of old obsolete and unused code, fixes the culture setting issue, simplifies the UmbracoBackOfficeIdentity since it no longer needs to be a FormsIdentity and just a straight forward ClaimsIdentity

This commit is contained in:
Shannon
2018-04-05 23:10:51 +10:00
parent 798236dc85
commit 262c4afb16
26 changed files with 766 additions and 1104 deletions

View File

@@ -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
{
/// <summary>
/// Extensions to create and renew and remove authentication tickets for the Umbraco back office
/// </summary>
public static class AuthenticationExtensions
{
/// <summary>
/// This will check the ticket to see if it is valid, if it is it will set the current thread's user and culture
/// </summary>
/// <param name="http"></param>
/// <param name="ticket"></param>
/// <param name="renewTicket">If true will attempt to renew the ticket</param>
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;
}
/// <summary>
/// This will return the current back office identity if the IPrincipal is the correct type
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
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<UmbracoBackOfficeIdentity>().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;
}
/// <summary>
/// This will return the current back office identity.
/// </summary>
/// <param name="http"></param>
/// <param name="authenticateRequestIfNotFound">
/// 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.
/// </param>
/// <returns>
/// Returns the current back office identity if an admin is authenticated otherwise null
/// </returns>
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;
}
/// <summary>
/// This will return the current back office identity.
/// </summary>
/// <param name="http"></param>
/// <param name="authenticateRequestIfNotFound">
/// 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
/// </param>
/// <returns>
/// Returns the current back office identity if an admin is authenticated otherwise null
/// </returns>
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);
}
/// <summary>
/// This clears the forms authentication cookie
/// </summary>
/// <param name="http"></param>
internal static void UmbracoLogout(this HttpContext http)
{
if (http == null) throw new ArgumentNullException("http");
new HttpContextWrapper(http).UmbracoLogout();
}
/// <summary>
/// This will force ticket renewal in the OWIN pipeline
/// </summary>
/// <param name="http"></param>
/// <returns></returns>
public static bool RenewUmbracoAuthTicket(this HttpContextBase http)
{
if (http == null) throw new ArgumentNullException("http");
http.Items[Constants.Security.ForceReAuthFlag] = true;
return true;
}
/// <summary>
/// This will force ticket renewal in the OWIN pipeline
/// </summary>
/// <param name="http"></param>
/// <returns></returns>
internal static bool RenewUmbracoAuthTicket(this HttpContext http)
{
if (http == null) throw new ArgumentNullException("http");
http.Items[Constants.Security.ForceReAuthFlag] = true;
return true;
}
/// <summary>
/// Creates the umbraco authentication ticket
/// </summary>
/// <param name="http"></param>
/// <param name="userdata"></param>
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);
}
/// <summary>
/// returns the number of seconds the user has until their auth session times out
/// </summary>
/// <param name="http"></param>
/// <returns></returns>
public static double GetRemainingAuthSeconds(this HttpContextBase http)
{
if (http == null) throw new ArgumentNullException("http");
var ticket = http.GetUmbracoAuthTicket();
return ticket.GetRemainingAuthSeconds();
}
/// <summary>
/// returns the number of seconds the user has until their auth session times out
/// </summary>
/// <param name="ticket"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Gets the umbraco auth ticket
/// </summary>
/// <param name="http"></param>
/// <returns></returns>
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;
}
}
/// <summary>
/// This clears the forms authentication cookie
/// </summary>
/// <param name="http"></param>
/// <param name="cookieName"></param>
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
{
/// <summary>
/// This will return the current back office identity if the IPrincipal is the correct type
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
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<UmbracoBackOfficeIdentity>().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<string, string>();
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<string, string> 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);
}
/// <summary>
/// Creates a custom FormsAuthentication ticket with the data specified
/// </summary>
/// <param name="http">The HTTP.</param>
/// <param name="username">The username.</param>
/// <param name="userData">The user data.</param>
/// <param name="loginTimeoutMins">The login timeout mins.</param>
/// <param name="minutesPersisted">The minutes persisted.</param>
/// <param name="cookieName">Name of the cookie.</param>
/// <param name="cookieDomain">The cookie domain.</param>
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;
}
/// <summary>
/// Ensures that the thread culture is set based on the back office user's culture
/// </summary>
/// <param name="identity"></param>
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;
}
/// <summary>
/// Used so that we aren't creating a new CultureInfo object for every single request
/// </summary>
private static readonly ConcurrentDictionary<string, CultureInfo> UserCultures = new ConcurrentDictionary<string, CultureInfo>();
}
}
/// <summary>
/// Ensures that the thread culture is set based on the back office user's culture
/// </summary>
/// <param name="identity"></param>
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));
}
}
/// <summary>
/// Used so that we aren't creating a new CultureInfo object for every single request
/// </summary>
private static readonly ConcurrentDictionary<string, CultureInfo> UserCultures = new ConcurrentDictionary<string, CultureInfo>();
}
}

View File

@@ -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<BackOfficeIdentityUser>

View File

@@ -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);
}

View File

@@ -31,16 +31,6 @@ namespace Umbraco.Core.Security
{
}
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Use the constructor specifying all dependencies instead")]
public BackOfficeUserManager(
IUserStore<BackOfficeIdentityUser, int> store,
IdentityFactoryOptions<BackOfficeUserManager> options,
MembershipProviderBase membershipProvider)
: this(store, options, membershipProvider, UmbracoConfig.For.UmbracoSettings().Content)
{
}
public BackOfficeUserManager(
IUserStore<BackOfficeIdentityUser, int> store,
IdentityFactoryOptions<BackOfficeUserManager> 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<BackOfficeUserManager> options,
BackOfficeUserStore customUserStore,
MembershipProviderBase membershipProvider)
{
var manager = new BackOfficeUserManager(customUserStore, options, membershipProvider);
return manager;
}
/// <summary>
/// Creates a BackOfficeUserManager instance with all default options and a custom BackOfficeUserManager instance
/// </summary>
@@ -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<BackOfficeUserManager> options)
{
InitUserManager(manager, membershipProvider, UmbracoConfig.For.UmbracoSettings().Content, options);
}
/// <summary>
/// Initializes the user manager with the correct options
/// </summary>
@@ -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<T> manager,
MembershipProviderBase membershipProvider,
IDataProtectionProvider dataProtectionProvider)
{
InitUserManager(manager, membershipProvider, dataProtectionProvider, UmbracoConfig.For.UmbracoSettings().Content);
}
/// <summary>
/// Initializes the user manager with the correct options
/// </summary>

View File

@@ -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<BackOfficeIdentityUser>)} from the {typeof (IOwinContext)}.");
}
}
}

View File

@@ -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.
/// </remarks>
[Serializable]
public class UmbracoBackOfficeIdentity : FormsIdentity
public class UmbracoBackOfficeIdentity : ClaimsIdentity
{
public static UmbracoBackOfficeIdentity FromClaimsIdentity(ClaimsIdentity identity)
{
return new UmbracoBackOfficeIdentity(identity);
}
/// <summary>
/// Creates a new UmbracoBackOfficeIdentity
/// </summary>
/// <param name="userId"></param>
/// <param name="username"></param>
/// <param name="realName"></param>
/// <param name="startContentNodes"></param>
/// <param name="startMediaNodes"></param>
/// <param name="culture"></param>
/// <param name="sessionId"></param>
/// <param name="securityStamp"></param>
/// <param name="allowedApps"></param>
/// <param name="roles"></param>
public UmbracoBackOfficeIdentity(int userId, string username, string realName,
IEnumerable<int> startContentNodes, IEnumerable<int> startMediaNodes, string culture,
string sessionId, string securityStamp, IEnumerable<string> allowedApps, IEnumerable<string> roles)
: base(Enumerable.Empty<Claim>(), 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);
}
/// <summary>
/// Creates a new UmbracoBackOfficeIdentity
/// </summary>
/// <param name="childIdentity">
/// The original identity created by the ClaimsIdentityFactory
/// </param>
/// <param name="userId"></param>
/// <param name="username"></param>
/// <param name="realName"></param>
/// <param name="startContentNodes"></param>
/// <param name="startMediaNodes"></param>
/// <param name="culture"></param>
/// <param name="sessionId"></param>
/// <param name="securityStamp"></param>
/// <param name="allowedApps"></param>
/// <param name="roles"></param>
public UmbracoBackOfficeIdentity(ClaimsIdentity childIdentity,
int userId, string username, string realName,
IEnumerable<int> startContentNodes, IEnumerable<int> startMediaNodes, string culture,
string sessionId, string securityStamp, IEnumerable<string> allowedApps, IEnumerable<string> 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);
}
/// <summary>
/// Create a back office identity based on an existing claims identity
/// </summary>
/// <param name="identity"></param>
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<int[]>(startContentId);
startMediaIdsAsInt = JsonConvert.DeserializeObject<int[]>(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);
}
/// <summary>
/// Create a back office identity based on user data
/// </summary>
/// <param name="userdata"></param>
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();
}
/// <summary>
/// Create a back office identity based on an existing claims identity
/// </summary>
/// <param name="claimsIdentity"></param>
/// <param name="userdata"></param>
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();
}
/// <summary>
/// Create a new identity from a forms auth ticket
/// </summary>
/// <param name="ticket"></param>
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<UserData>(ticket.UserData);
AddUserDataClaims();
}
/// <summary>
/// Used for cloning
/// </summary>
/// <param name="identity"></param>
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;
/// <summary>
/// Used during ctor to add existing claims from an existing ClaimsIdentity
/// </summary>
/// <param name="claimsIdentity"></param>
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;
/// <summary>
/// Returns the required claim types for a back office identity
@@ -179,153 +115,125 @@ namespace Umbraco.Core.Security
/// <remarks>
/// This does not incude the role claim type or allowed apps type since that is a collection and in theory could be empty
/// </remarks>
public static IEnumerable<string> RequiredBackOfficeIdentityClaimTypes
public static IEnumerable<string> 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
};
/// <summary>
/// Adds claims based on the UserData data
/// Adds claims based on the ctor data
/// </summary>
private void AddUserDataClaims()
private void AddRequiredClaims(int userId, string username, string realName,
IEnumerable<int> startContentNodes, IEnumerable<int> startMediaNodes, string culture,
string sessionId, string securityStamp, IEnumerable<string> allowedApps, IEnumerable<string> 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; }
/// <inheritdoc />
/// <summary>
/// Gets the type of authenticated identity.
/// </summary>
/// <returns>
/// The type of authenticated identity. This property always returns "UmbracoBackOffice".
/// </returns>
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; }
}
/// <summary>
/// Gets a copy of the current <see cref="T:UmbracoBackOfficeIdentity"/> instance.
/// </summary>
/// <returns>
/// A copy of the current <see cref="T:UmbracoBackOfficeIdentity"/> instance.
/// </returns>
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();
}
}

View File

@@ -1,74 +0,0 @@
using System;
using System.Runtime.Serialization;
namespace Umbraco.Core.Security
{
/// <summary>
/// Data structure used to store information in the authentication cookie
/// </summary>
[DataContract(Name = "userData", Namespace = "")]
[Serializable]
public class UserData
{
public UserData()
{
AllowedApplications = new string[] {};
Roles = new string[] {};
}
/// <summary>
/// Use this constructor to create/assign new UserData to the ticket
/// </summary>
/// <param name="sessionId">
/// The current sessionId for the user
/// </param>
public UserData(string sessionId)
{
SessionId = sessionId;
AllowedApplications = new string[] { };
Roles = new string[] { };
}
/// <summary>
/// Gets or sets the session identifier.
/// </summary>
[DataMember(Name = "sessionId")]
public string SessionId { get; set; }
/// <summary>
/// Gets or sets the security stamp.
/// </summary>
[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; }
/// <summary>
/// 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
/// </summary>
[DataMember(Name = "startContent")]
public int[] StartContentNodes { get; set; }
/// <summary>
/// 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
/// </summary>
[DataMember(Name = "startMedia")]
public int[] StartMediaNodes { get; set; }
[DataMember(Name = "allowedApps")]
public string[] AllowedApplications { get; set; }
[DataMember(Name = "culture")]
public string Culture { get; set; }
}
}

View File

@@ -1330,7 +1330,6 @@
<Compile Include="Security\UmbracoEmailMessage.cs" />
<Compile Include="Security\UmbracoMembershipProviderBase.cs" />
<Compile Include="Security\UserAwareMembershipProviderPasswordHasher.cs" />
<Compile Include="Security\UserData.cs" />
<Compile Include="SemVersionExtensions.cs" />
<Compile Include="Serialization\AbstractSerializationService.cs" />
<Compile Include="Serialization\ForceInt32Converter.cs" />

View File

@@ -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<InvalidOperationException>(() => 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());
}
}

View File

@@ -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()

View File

@@ -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;

View File

@@ -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
{
/// <summary>
/// Extensions to create and renew and remove authentication tickets for the Umbraco back office
/// </summary>
public static class AuthenticationExtensions
{
/// <summary>
/// This will check the ticket to see if it is valid, if it is it will set the current thread's user and culture
/// </summary>
/// <param name="http"></param>
/// <param name="ticket"></param>
/// <param name="renewTicket">If true will attempt to renew the ticket</param>
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;
}
/// <summary>
/// This will return the current back office identity.
/// </summary>
/// <param name="http"></param>
/// <param name="authenticateRequestIfNotFound">
/// 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.
/// </param>
/// <returns>
/// Returns the current back office identity if an admin is authenticated otherwise null
/// </returns>
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;
}
/// <summary>
/// This will return the current back office identity.
/// </summary>
/// <param name="http"></param>
/// <param name="authenticateRequestIfNotFound">
/// 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
/// </param>
/// <returns>
/// Returns the current back office identity if an admin is authenticated otherwise null
/// </returns>
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);
}
/// <summary>
/// This clears the forms authentication cookie
/// </summary>
/// <param name="http"></param>
internal static void UmbracoLogout(this HttpContext http)
{
if (http == null) throw new ArgumentNullException("http");
new HttpContextWrapper(http).UmbracoLogout();
}
/// <summary>
/// This will force ticket renewal in the OWIN pipeline
/// </summary>
/// <param name="http"></param>
/// <returns></returns>
public static bool RenewUmbracoAuthTicket(this HttpContextBase http)
{
if (http == null) throw new ArgumentNullException("http");
http.Items[Constants.Security.ForceReAuthFlag] = true;
return true;
}
/// <summary>
/// This will force ticket renewal in the OWIN pipeline
/// </summary>
/// <param name="http"></param>
/// <returns></returns>
internal static bool RenewUmbracoAuthTicket(this HttpContext http)
{
if (http == null) throw new ArgumentNullException("http");
http.Items[Constants.Security.ForceReAuthFlag] = true;
return true;
}
///// <summary>
///// Creates the umbraco authentication ticket
///// </summary>
///// <param name="http"></param>
///// <param name="userdata"></param>
//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);
//}
/// <summary>
/// returns the number of seconds the user has until their auth session times out
/// </summary>
/// <param name="http"></param>
/// <returns></returns>
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();
}
/// <summary>
/// returns the number of seconds the user has until their auth session times out
/// </summary>
/// <param name="ticket"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Gets the umbraco auth ticket
/// </summary>
/// <param name="http"></param>
/// <returns></returns>
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);
}
/// <summary>
/// This clears the forms authentication cookie
/// </summary>
/// <param name="http"></param>
/// <param name="cookieName"></param>
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<string, string>();
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<string, string>();
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<AuthenticationTicket> secureDataFormat, IDictionary<string, string> 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);
}
}
}

View File

@@ -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<GetUserSecondsMiddleWare>(
getSecondsOptions,
cookieAuthOptions,
UmbracoConfig.For.UmbracoSettings().Security,
app.CreateLogger<GetUserSecondsMiddleWare>());
//This is required so that we can read the auth ticket format outside of this pipeline
app.CreatePerOwinContext<UmbracoAuthTicketDataProtector>(
(options, context) => new UmbracoAuthTicketDataProtector(cookieOptions.TicketDataFormat));
return app;
}
@@ -346,11 +352,19 @@ namespace Umbraco.Web.Security.Identity
/// <returns></returns>
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;
}
}

View File

@@ -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
{

View File

@@ -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
{
/// <summary>
/// Custom secure format that uses the old FormsAuthentication format
/// </summary>
internal class FormsAuthenticationSecureDataFormat : ISecureDataFormat<AuthenticationTicket>
{
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);
}
/// <summary>
/// Unprotects the cookie
/// </summary>
/// <param name="protectedText"></param>
/// <returns></returns>
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;
}
}
}

View File

@@ -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)

View File

@@ -14,29 +14,19 @@ namespace Umbraco.Web.Security.Identity
/// </summary>
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<AuthenticationTicket> 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)
{
}
/// <summary>
/// Creates the cookie options for saving the auth cookie
/// </summary>
@@ -66,8 +49,8 @@ namespace Umbraco.Web.Security.Identity
/// <returns></returns>
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);

View File

@@ -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
{
/// <summary>
/// Custom secure format that ensures the Identity in the ticket is <see cref="UmbracoBackOfficeIdentity"/> and not just a ClaimsIdentity
/// </summary>
internal class UmbracoSecureDataFormat : ISecureDataFormat<AuthenticationTicket>
{
private readonly int _loginTimeoutMinutes;
private readonly ISecureDataFormat<AuthenticationTicket> _ticketDataFormat;
public UmbracoSecureDataFormat(int loginTimeoutMinutes, ISecureDataFormat<AuthenticationTicket> 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);
}
/// <summary>
/// Unprotects the cookie
/// </summary>
/// <param name="protectedText"></param>
/// <returns></returns>
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;
}
}
}

View File

@@ -1,49 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Umbraco.Web.Security
{
/// <summary>
/// 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
/// </summary>
/// <remarks>
/// 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)
/// </remarks>
public static class LegacyDefaultAppMapping
{
/// <summary>
/// Constructor that assigns all initial known mappings
/// </summary>
static LegacyDefaultAppMapping()
{
}
private static readonly ConcurrentDictionary<string, string> NodeTypeAliasMapping = new ConcurrentDictionary<string, string>();
/// <summary>
/// Adds the default app mapping to the node type
/// </summary>
/// <param name="nodeType">The nodeType is the same nodeType found in the UI.xml</param>
/// <param name="defaultApp">The default app associated with this nodeType if the 'app' parameter was not detected</param>
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;
}
}
}

View File

@@ -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
{
/// <summary>
/// Nasty little hack to get httpcontextbase from an owin context
/// Gets the <see cref="ISecureDataFormat{AuthenticationTicket}"/> for the Umbraco back office cookie
/// </summary>
/// <param name="owinContext"></param>
/// <returns></returns>
internal static Attempt<HttpContextBase> TryGetHttpContext(this IOwinContext owinContext)
internal static ISecureDataFormat<AuthenticationTicket> GetUmbracoAuthTicketDataProtector(this IOwinContext owinContext)
{
var ctx = owinContext.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
return ctx == null ? Attempt<HttpContextBase>.Fail() : Attempt.Succeed(ctx);
return owinContext.Get<UmbracoAuthTicketDataProtector>().Protector;
}
}

View File

@@ -0,0 +1,19 @@
using System;
using Microsoft.Owin.Security;
using Umbraco.Core;
namespace Umbraco.Web.Security
{
/// <summary>
/// This is used so that we can retrive the auth ticket protector from an IOwinContext
/// </summary>
internal class UmbracoAuthTicketDataProtector : DisposableObjectSlim
{
public UmbracoAuthTicketDataProtector(ISecureDataFormat<AuthenticationTicket> protector)
{
Protector = protector ?? throw new ArgumentNullException(nameof(protector));
}
public ISecureDataFormat<AuthenticationTicket> Protector { get; }
}
}

View File

@@ -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

View File

@@ -404,6 +404,9 @@
<Compile Include="Search\SearchableTreeCollection.cs" />
<Compile Include="Search\SearchableTreeCollectionBuilder.cs" />
<Compile Include="Search\UmbracoTreeSearcher.cs" />
<Compile Include="Security\AuthenticationExtensions.cs" />
<Compile Include="Security\Identity\UmbracoSecureDataFormat.cs" />
<Compile Include="Security\UmbracoAuthTicketDataProtector.cs" />
<Compile Include="SignalR\IPreviewHub.cs" />
<Compile Include="SignalR\PreviewHub.cs" />
<Compile Include="SignalR\PreviewHubComponent.cs" />
@@ -769,7 +772,6 @@
<Compile Include="Security\Identity\AuthenticationOptionsExtensions.cs" />
<Compile Include="Security\Identity\AuthenticationManagerExtensions.cs" />
<Compile Include="Security\Identity\BackOfficeCookieManager.cs" />
<Compile Include="Security\Identity\FormsAuthenticationSecureDataFormat.cs" />
<Compile Include="Security\Identity\UmbracoBackOfficeCookieAuthOptions.cs" />
<Compile Include="Scheduling\TaskAndFactoryExtensions.cs" />
<Compile Include="Migrations\ClearCsrfCookiesAfterUpgrade.cs" />
@@ -1084,7 +1086,6 @@
<Compile Include="Routing\IUrlProvider.cs" />
<Compile Include="Routing\SiteDomainHelper.cs" />
<Compile Include="Routing\EnsureRoutableOutcome.cs" />
<Compile Include="Security\LegacyDefaultAppMapping.cs" />
<Compile Include="Routing\PublishedRouter.cs" />
<Compile Include="Routing\UrlProvider.cs" />
<Compile Include="Routing\WebServicesRouteConstraint.cs" />

View File

@@ -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));

View File

@@ -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

View File

@@ -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
{