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.AspNetCore.Identity; using Microsoft.Owin; using Microsoft.Owin.Security; using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.BackOffice; using Umbraco.Extensions; using Umbraco.Web.Composing; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Security { /// /// Extensions to create and renew and remove authentication tickets for the Umbraco back office /// public static class AuthenticationExtensions { /// /// This will check the ticket to see if it is valid, if it is it will set the current thread's user and culture /// /// /// /// If true will attempt to renew the ticket public static bool AuthenticateCurrentRequest(this HttpContextBase http, AuthenticationTicket ticket, bool renewTicket) { if (http == null) throw new ArgumentNullException(nameof(http)); // if there was a ticket, it's not expired, - it should not be renewed or its renewable if (ticket?.Properties.ExpiresUtc != null && ticket.Properties.ExpiresUtc.Value > DateTimeOffset.UtcNow && (renewTicket == false || http.RenewUmbracoAuthTicket())) { try { // get the Umbraco user identity if (!(ticket.Identity is UmbracoBackOfficeIdentity identity)) throw new InvalidOperationException("The AuthenticationTicket specified does not contain the correct Identity type"); // set the principal object var principal = new ClaimsPrincipal(identity); // It is actually not good enough to set this on the current app Context and the thread, it also needs // to be set explicitly on the HttpContext.Current !! This is a strange web api thing that is actually // an underlying fault of asp.net not propagating the User correctly. if (HttpContext.Current != null) { HttpContext.Current.User = principal; } http.User = principal; Thread.CurrentPrincipal = principal; // This is a back office request, we will also set the culture/ui culture Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo(identity.Culture); return true; } catch (Exception ex) { if (ex is FormatException || ex is JsonReaderException) { // this will occur if the cookie data is invalid http.UmbracoLogout(); } else { throw; } } } return false; } /// /// This will return the current back office identity. /// /// /// /// If set to true and a back office identity is not found and not authenticated, this will attempt to authenticate the /// request just as is done in the Umbraco module and then set the current identity if it is valid. /// Just like in the UmbracoModule, if this is true then the user's culture will be assigned to the request. /// /// /// Returns the current back office identity if an admin is authenticated otherwise null /// public static UmbracoBackOfficeIdentity GetCurrentIdentity(this HttpContextBase http, bool authenticateRequestIfNotFound) { if (http == null) throw new ArgumentNullException(nameof(http)); if (http.User == null) return null; //there's no user at all so no identity // If it's already a UmbracoBackOfficeIdentity var backOfficeIdentity = http.User.GetUmbracoIdentity(); if (backOfficeIdentity != null) return backOfficeIdentity; if (authenticateRequestIfNotFound == false) return null; // even if authenticateRequestIfNotFound is true we cannot continue if the request is actually authenticated // which would mean something strange is going on that it is not an umbraco identity. if (http.User.Identity.IsAuthenticated) return null; // So the user is not authed but we've been asked to do the auth if authenticateRequestIfNotFound = true, // which might occur in old webforms style things or for routes that aren't included as a back office request. // in this case, we are just reverting to authing using the cookie. // TODO: Even though this is in theory legacy, we have legacy bits laying around and we'd need to do the auth based on // how the Module will eventually do it (by calling in to any registered authenticators). var ticket = http.GetUmbracoAuthTicket(); if (http.AuthenticateCurrentRequest(ticket, true)) { //now we 'should have an umbraco identity return http.User.Identity as UmbracoBackOfficeIdentity; } return null; } /// /// This will return the current back office identity. /// /// /// /// If set to true and a back office identity is not found and not authenticated, this will attempt to authenticate the /// request just as is done in the Umbraco module and then set the current identity if it is valid /// /// /// Returns the current back office identity if an admin is authenticated otherwise null /// internal static UmbracoBackOfficeIdentity GetCurrentIdentity(this HttpContext http, bool authenticateRequestIfNotFound) { if (http == null) throw new ArgumentNullException("http"); return new HttpContextWrapper(http).GetCurrentIdentity(authenticateRequestIfNotFound); } public static void UmbracoLogout(this HttpContextBase http) { if (http == null) throw new ArgumentNullException("http"); Logout(http, Current.Configs.Security().AuthCookieName); } /// /// This clears the forms authentication cookie /// /// internal static void UmbracoLogout(this HttpContext http) { if (http == null) throw new ArgumentNullException("http"); new HttpContextWrapper(http).UmbracoLogout(); } /// /// This will force ticket renewal in the OWIN pipeline /// /// /// public static bool RenewUmbracoAuthTicket(this HttpContextBase http) { if (http == null) throw new ArgumentNullException("http"); http.Items[Constants.Security.ForceReAuthFlag] = true; return true; } /// /// returns the number of seconds the user has until their auth session times out /// /// /// public static double GetRemainingAuthSeconds(this HttpContextBase http) { if (http == null) throw new ArgumentNullException(nameof(http)); var ticket = http.GetUmbracoAuthTicket(); return ticket.GetRemainingAuthSeconds(); } /// /// returns the number of seconds the user has until their auth session times out /// /// /// public static double GetRemainingAuthSeconds(this AuthenticationTicket ticket) { var utcExpired = ticket?.Properties.ExpiresUtc; if (utcExpired == null) return 0; var secondsRemaining = utcExpired.Value.Subtract(DateTimeOffset.UtcNow).TotalSeconds; return secondsRemaining; } /// /// Gets the umbraco auth ticket /// /// /// public static AuthenticationTicket GetUmbracoAuthTicket(this HttpContextBase http) { if (http == null) throw new ArgumentNullException(nameof(http)); return GetAuthTicket(http, Current.Configs.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, Current.Configs.Security().AuthCookieName); } /// /// This clears the forms authentication cookie /// /// /// private static void Logout(this HttpContextBase http, string cookieName) { // We need to clear the sessionId from the database. This is legacy code to do any logging out and shouldn't really be used at all but in any case // we need to make sure the session is cleared. Due to the legacy nature of this it means we need to use singletons if (http.User != null) { var claimsIdentity = http.User.Identity as ClaimsIdentity; if (claimsIdentity != null) { var sessionId = claimsIdentity.FindFirstValue(Constants.Security.SessionIdClaimType); Guid guidSession; if (sessionId.IsNullOrWhiteSpace() == false && Guid.TryParse(sessionId, out guidSession)) { Current.Services.UserService.ClearLoginSession(guidSession); } } } if (http == null) throw new ArgumentNullException("http"); // clear the preview cookie and external login var cookies = new[] { cookieName, Constants.Web.PreviewCookieName, Constants.Security.BackOfficeExternalCookieName }; foreach (var c in cookies) { // remove from the request http.Request.Cookies.Remove(c); // expire from the response var formsCookie = http.Response.Cookies[c]; if (formsCookie != null) { // this will expire immediately and be removed from the browser formsCookie.Expires = DateTime.Now.AddYears(-1); } else { // ensure there's def an expired cookie http.Response.Cookies.Add(new HttpCookie(c) { Expires = DateTime.Now.AddYears(-1) }); } } } private static AuthenticationTicket GetAuthTicket(this IOwinContext owinCtx, string cookieName) { var asDictionary = new Dictionary(); foreach (var requestCookie in owinCtx.Request.Cookies) { var key = requestCookie.Key; asDictionary[key] = requestCookie.Value; } var secureFormat = owinCtx.GetUmbracoAuthTicketDataProtector(); // get the ticket try { return GetAuthTicket(secureFormat, asDictionary, cookieName); } catch (Exception) { owinCtx.Authentication.SignOut( Constants.Security.BackOfficeAuthenticationType, Constants.Security.BackOfficeExternalAuthenticationType); return null; } } private static AuthenticationTicket GetAuthTicket(this HttpContextBase http, string cookieName) { var asDictionary = new Dictionary(); for (var i = 0; i < http.Request.Cookies.Keys.Count; i++) { var key = http.Request.Cookies.Keys.Get(i); asDictionary[key] = http.Request.Cookies[key].Value; } var owinCtx = http.GetOwinContext(); var secureFormat = owinCtx.GetUmbracoAuthTicketDataProtector(); // will only happen in tests if (secureFormat == null) return null; // get the ticket try { return GetAuthTicket(secureFormat, asDictionary, cookieName); } catch (Exception) { // occurs when decryption fails http.Logout(cookieName); return null; } } private static AuthenticationTicket GetAuthTicket(ISecureDataFormat secureDataFormat, IDictionary cookies, string cookieName) { if (cookies == null) throw new ArgumentNullException(nameof(cookies)); if (cookies.ContainsKey(cookieName) == false) return null; var formsCookie = cookies[cookieName]; if (formsCookie == null) { return null; } // get the ticket return secureDataFormat.Unprotect(formsCookie); } /// /// Ensures that the thread culture is set based on the back office user's culture /// /// public static void EnsureCulture(this IIdentity identity) { if (identity is UmbracoBackOfficeIdentity umbIdentity && umbIdentity.IsAuthenticated) { Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = UserCultures.GetOrAdd(umbIdentity.Culture, s => new CultureInfo(s)); } } /// /// Used so that we aren't creating a new CultureInfo object for every single request /// private static readonly ConcurrentDictionary UserCultures = new ConcurrentDictionary(); } }