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