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:
@@ -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>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
378
src/Umbraco.Web/Security/AuthenticationExtensions.cs
Normal file
378
src/Umbraco.Web/Security/AuthenticationExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
83
src/Umbraco.Web/Security/Identity/UmbracoSecureDataFormat.cs
Normal file
83
src/Umbraco.Web/Security/Identity/UmbracoSecureDataFormat.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
19
src/Umbraco.Web/Security/UmbracoAuthTicketDataProtector.cs
Normal file
19
src/Umbraco.Web/Security/UmbracoAuthTicketDataProtector.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user