using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; 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 AutoMapper; using Microsoft.Owin; using Newtonsoft.Json; using Umbraco.Core.Configuration; using Umbraco.Core.Models.Membership; using Microsoft.Owin; using Umbraco.Core.Logging; namespace Umbraco.Core.Security { /// /// Extensions to create and renew and remove authentication tickets for the Umbraco back office /// public static class AuthenticationExtensions { /// /// This will check the ticket to see if it is valid, if it is it will set the current thread's user and culture /// /// /// /// If true will attempt to renew the ticket public static bool AuthenticateCurrentRequest(this HttpContextBase http, FormsAuthenticationTicket ticket, bool renewTicket) { if (http == null) throw new ArgumentNullException("http"); //if there was a ticket, it's not expired, - it should not be renewed or its renewable if (ticket != null && ticket.Expired == false && (renewTicket == false || http.RenewUmbracoAuthTicket())) { try { //create the Umbraco user identity var identity = new UmbracoBackOfficeIdentity(ticket); //set the principal object var principal = new GenericPrincipal(identity, identity.Roles); //It is actually not good enough to set this on the current app Context and the thread, it also needs // to be set explicitly on the HttpContext.Current !! This is a strange web api thing that is actually // an underlying fault of asp.net not propogating the User correctly. if (HttpContext.Current != null) { HttpContext.Current.User = principal; } http.User = principal; Thread.CurrentPrincipal = principal; //This is a back office request, we will also set the culture/ui culture Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo(identity.Culture); return true; } catch (Exception ex) { if (ex is FormatException || ex is JsonReaderException) { //this will occur if the cookie data is invalid http.UmbracoLogout(); } else { throw; } } } return false; } /// /// This will return the current back office identity. /// /// /// /// If set to true and a back office identity is not found and not authenticated, this will attempt to authenticate the /// request just as is done in the Umbraco module and then set the current identity if it is valid. /// Just like in the UmbracoModule, if this is true then the user's culture will be assigned to the request. /// /// /// Returns the current back office identity if an admin is authenticated otherwise null /// public static UmbracoBackOfficeIdentity GetCurrentIdentity(this HttpContextBase http, bool authenticateRequestIfNotFound) { if (http == null) throw new ArgumentNullException("http"); if (http.User == null) return null; //there's no user at all so no identity //If it's already a UmbracoBackOfficeIdentity var backOfficeIdentity = http.User.Identity as UmbracoBackOfficeIdentity; if (backOfficeIdentity != null) return backOfficeIdentity; //Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd and has the back office session var claimsIdentity = http.User.Identity as ClaimsIdentity; if (claimsIdentity != null && claimsIdentity.IsAuthenticated) { try { return UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity); } catch (InvalidOperationException ex) { //This will occur if the required claim types are missing which would mean something strange is going on LogHelper.Error(typeof(AuthenticationExtensions), "The current identity cannot be converted to " + typeof(UmbracoBackOfficeIdentity), ex); } } 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); } /// /// This clears the forms authentication cookie /// public static void UmbracoLogout(this HttpContextBase http) { if (http == null) throw new ArgumentNullException("http"); Logout(http, UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName); } /// /// This clears the forms authentication cookie for webapi since cookies are handled differently /// /// public static void UmbracoLogoutWebApi(this HttpResponseMessage response) { if (response == null) throw new ArgumentNullException("response"); //remove the cookie var authCookie = new CookieHeaderValue(UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, "") { Expires = DateTime.Now.AddYears(-1), Path = "/" }; //remove the preview cookie too var prevCookie = new CookieHeaderValue(Constants.Web.PreviewCookieName, "") { Expires = DateTime.Now.AddYears(-1), Path = "/" }; //remove the external login cookie too var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalCookieName, "") { Expires = DateTime.Now.AddYears(-1), Path = "/" }; response.Headers.AddCookies(new[] { authCookie, prevCookie, extLoginCookie }); } /// /// This adds the forms authentication cookie for webapi since cookies are handled differently /// /// /// public static FormsAuthenticationTicket UmbracoLoginWebApi(this HttpResponseMessage response, IUser user) { if (response == null) throw new ArgumentNullException("response"); //remove the external login cookie var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalCookieName, "") { Expires = DateTime.Now.AddYears(-1), Path = "/" }; var userDataString = JsonConvert.SerializeObject(Mapper.Map(user)); var ticket = new FormsAuthenticationTicket( 4, user.Username, DateTime.Now, DateTime.Now.AddMinutes(GlobalSettings.TimeOutInMinutes), true, userDataString, "/" ); // Encrypt the cookie using the machine key for secure transport var encrypted = FormsAuthentication.Encrypt(ticket); //add the cookie var authCookie = new CookieHeaderValue(UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, encrypted) { //Umbraco has always persisted it's original cookie for 1 day so we'll keep it that way Expires = DateTime.Now.AddMinutes(1440), Path = "/", Secure = GlobalSettings.UseSSL, HttpOnly = true }; response.Headers.AddCookies(new[] { authCookie, extLoginCookie }); return ticket; } /// /// This clears the forms authentication cookie /// /// internal static void UmbracoLogout(this HttpContext http) { if (http == null) throw new ArgumentNullException("http"); new HttpContextWrapper(http).UmbracoLogout(); } /// /// Renews the Umbraco authentication ticket /// /// /// public static bool RenewUmbracoAuthTicket(this HttpContextBase http) { if (http == null) throw new ArgumentNullException("http"); return RenewAuthTicket(http, UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, UmbracoConfig.For.UmbracoSettings().Security.AuthCookieDomain, //Umbraco has always persisted it's original cookie for 1 day so we'll keep it that way 1440); } internal static bool RenewUmbracoAuthTicket(this HttpContext http) { if (http == null) throw new ArgumentNullException("http"); return new HttpContextWrapper(http).RenewUmbracoAuthTicket(); } /// /// Creates the umbraco authentication ticket /// /// /// public static FormsAuthenticationTicket CreateUmbracoAuthTicket(this HttpContextBase http, UserData userdata) { 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); } internal static FormsAuthenticationTicket CreateUmbracoAuthTicket(this HttpContext http, UserData userdata) { if (http == null) throw new ArgumentNullException("http"); if (userdata == null) throw new ArgumentNullException("userdata"); return new HttpContextWrapper(http).CreateUmbracoAuthTicket(userdata); } /// /// returns the number of seconds the user has until their auth session times out /// /// /// public static double GetRemainingAuthSeconds(this HttpContextBase http) { if (http == null) throw new ArgumentNullException("http"); var ticket = http.GetUmbracoAuthTicket(); return ticket.GetRemainingAuthSeconds(); } /// /// returns the number of seconds the user has until their auth session times out /// /// /// public static double GetRemainingAuthSeconds(this FormsAuthenticationTicket ticket) { if (ticket == null) { return 0; } var utcExpired = ticket.Expiration.ToUniversalTime(); var secondsRemaining = utcExpired.Subtract(DateTime.UtcNow).TotalSeconds; return secondsRemaining; } /// /// Gets the umbraco auth ticket /// /// /// public static FormsAuthenticationTicket GetUmbracoAuthTicket(this HttpContextBase http) { if (http == null) throw new ArgumentNullException("http"); return GetAuthTicket(http, UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName); } internal static FormsAuthenticationTicket GetUmbracoAuthTicket(this HttpContext http) { if (http == null) throw new ArgumentNullException("http"); return new HttpContextWrapper(http).GetUmbracoAuthTicket(); } internal static FormsAuthenticationTicket GetUmbracoAuthTicket(this IOwinContext ctx) { if (ctx == null) throw new ArgumentNullException("ctx"); //get the ticket try { return GetAuthTicket(ctx.Request.Cookies.ToDictionary(x => x.Key, x => x.Value), UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName); } catch (Exception) { //TODO: Do we need to do more here?? need to make sure that the forms cookie is gone, but is that // taken care of in our custom middleware somehow? ctx.Authentication.SignOut(); return null; } } /// /// This clears the forms authentication cookie /// /// /// private static void Logout(this HttpContextBase http, string cookieName) { if (http == null) throw new ArgumentNullException("http"); //clear the preview cookie and external login var cookies = new[] { cookieName, Constants.Web.PreviewCookieName, Constants.Security.BackOfficeExternalCookieName }; foreach (var c in cookies) { //remove from the request http.Request.Cookies.Remove(c); //expire from the response var formsCookie = http.Response.Cookies[c]; if (formsCookie != null) { //this will expire immediately and be removed from the browser formsCookie.Expires = DateTime.Now.AddYears(-1); } else { //ensure there's def an expired cookie http.Response.Cookies.Add(new HttpCookie(c) { Expires = DateTime.Now.AddYears(-1) }); } } } private static FormsAuthenticationTicket GetAuthTicket(this HttpContextBase http, string cookieName) { var asDictionary = new Dictionary(); for (var i = 0; i < http.Request.Cookies.Keys.Count; i++) { var key = http.Request.Cookies.Keys.Get(i); asDictionary[key] = http.Request.Cookies[key].Value; } //get the ticket try { return GetAuthTicket(asDictionary, cookieName); } catch (Exception) { //occurs when decryption fails http.Logout(cookieName); return null; } } private static FormsAuthenticationTicket GetAuthTicket(IDictionary cookies, string cookieName) { if (cookies == null) throw new ArgumentNullException("cookies"); if (cookies.ContainsKey(cookieName) == false) return null; var formsCookie = cookies[cookieName]; if (formsCookie == null) { return null; } //get the ticket return FormsAuthentication.Decrypt(formsCookie); } /// /// Renews the forms authentication ticket & cookie /// /// /// /// /// /// true if there was a ticket to renew otherwise false if there was no ticket private static bool RenewAuthTicket(this HttpContextBase http, string cookieName, string cookieDomain, int minutesPersisted) { if (http == null) throw new ArgumentNullException("http"); //get the ticket var ticket = GetAuthTicket(http, cookieName); //renew the ticket var renewed = FormsAuthentication.RenewTicketIfOld(ticket); if (renewed == null) { return false; } //get the request cookie to get it's expiry date, //NOTE: this will never be null becaues we already do this // check in teh GetAuthTicket. var formsCookie = http.Request.Cookies[cookieName]; //encrypt it var hash = FormsAuthentication.Encrypt(renewed); //write it to the response var cookie = new HttpCookie(cookieName, hash) { Expires = DateTime.Now.AddMinutes(minutesPersisted), Domain = cookieDomain }; if (GlobalSettings.UseSSL) cookie.Secure = true; //ensure http only, this should only be able to be accessed via the server cookie.HttpOnly = true; //rewrite the cooke http.Response.Cookies.Set(cookie); return true; } /// /// Creates a custom FormsAuthentication ticket with the data specified /// /// The HTTP. /// The username. /// The user data. /// The login timeout mins. /// The minutes persisted. /// Name of the cookie. /// The cookie domain. private static FormsAuthenticationTicket CreateAuthTicketAndCookie(this HttpContextBase http, string username, string userData, int loginTimeoutMins, int minutesPersisted, string cookieName, string cookieDomain) { if (http == null) throw new ArgumentNullException("http"); // Create a new ticket used for authentication var ticket = new FormsAuthenticationTicket( 4, username, DateTime.Now, DateTime.Now.AddMinutes(loginTimeoutMins), true, userData, "/" ); // Encrypt the cookie using the machine key for secure transport var hash = FormsAuthentication.Encrypt(ticket); var cookie = new HttpCookie( cookieName, hash) { Expires = DateTime.Now.AddMinutes(minutesPersisted), Domain = cookieDomain, Path = "/" }; if (GlobalSettings.UseSSL) cookie.Secure = true; //ensure http only, this should only be able to be accessed via the server cookie.HttpOnly = true; http.Response.Cookies.Set(cookie); return ticket; } } }