2018-04-05 23:10:51 +10:00
|
|
|
|
using System;
|
2020-01-07 13:50:38 +01:00
|
|
|
|
using System.Collections.Concurrent;
|
2018-04-05 23:10:51 +10:00
|
|
|
|
using System.Collections.Generic;
|
2020-01-07 13:50:38 +01:00
|
|
|
|
using System.Globalization;
|
|
|
|
|
|
using System.Linq;
|
2018-04-05 23:10:51 +10:00
|
|
|
|
using System.Security.Claims;
|
2020-01-07 13:50:38 +01:00
|
|
|
|
using System.Security.Principal;
|
2018-04-05 23:10:51 +10:00
|
|
|
|
using System.Threading;
|
|
|
|
|
|
using System.Web;
|
2020-05-04 18:49:02 +01:00
|
|
|
|
using Microsoft.AspNetCore.Identity;
|
2018-04-05 23:10:51 +10:00
|
|
|
|
using Microsoft.Owin;
|
|
|
|
|
|
using Microsoft.Owin.Security;
|
|
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
|
|
using Umbraco.Core;
|
2020-05-18 08:21:34 +01:00
|
|
|
|
using Umbraco.Core.BackOffice;
|
|
|
|
|
|
using Umbraco.Extensions;
|
2019-12-19 15:53:50 +01:00
|
|
|
|
using Umbraco.Web.Composing;
|
2018-04-05 23:10:51 +10:00
|
|
|
|
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));
|
|
|
|
|
|
|
2019-01-26 10:52:19 -05:00
|
|
|
|
// if there was a ticket, it's not expired, - it should not be renewed or its renewable
|
2018-04-05 23:10:51 +10:00
|
|
|
|
if (ticket?.Properties.ExpiresUtc != null && ticket.Properties.ExpiresUtc.Value > DateTimeOffset.UtcNow && (renewTicket == false || http.RenewUmbracoAuthTicket()))
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2019-01-26 10:52:19 -05:00
|
|
|
|
// get the Umbraco user identity
|
2018-04-05 23:10:51 +10:00
|
|
|
|
if (!(ticket.Identity is UmbracoBackOfficeIdentity identity))
|
|
|
|
|
|
throw new InvalidOperationException("The AuthenticationTicket specified does not contain the correct Identity type");
|
|
|
|
|
|
|
2019-01-26 10:52:19 -05:00
|
|
|
|
// set the principal object
|
2018-04-05 23:10:51 +10:00
|
|
|
|
var principal = new ClaimsPrincipal(identity);
|
|
|
|
|
|
|
2019-01-26 10:52:19 -05:00
|
|
|
|
// It is actually not good enough to set this on the current app Context and the thread, it also needs
|
2018-04-05 23:10:51 +10:00
|
|
|
|
// to be set explicitly on the HttpContext.Current !! This is a strange web api thing that is actually
|
2019-01-26 10:52:19 -05:00
|
|
|
|
// an underlying fault of asp.net not propagating the User correctly.
|
2018-04-05 23:10:51 +10:00
|
|
|
|
if (HttpContext.Current != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
HttpContext.Current.User = principal;
|
|
|
|
|
|
}
|
|
|
|
|
|
http.User = principal;
|
|
|
|
|
|
Thread.CurrentPrincipal = principal;
|
|
|
|
|
|
|
2019-01-26 10:52:19 -05:00
|
|
|
|
// This is a back office request, we will also set the culture/ui culture
|
2018-04-05 23:10:51 +10:00
|
|
|
|
Thread.CurrentThread.CurrentCulture =
|
|
|
|
|
|
Thread.CurrentThread.CurrentUICulture =
|
|
|
|
|
|
new System.Globalization.CultureInfo(identity.Culture);
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (ex is FormatException || ex is JsonReaderException)
|
|
|
|
|
|
{
|
2019-01-26 10:52:19 -05:00
|
|
|
|
// this will occur if the cookie data is invalid
|
2020-06-02 13:28:30 +10:00
|
|
|
|
|
2018-04-05 23:10:51 +10:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
throw;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// This will return the current back office identity.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="http"></param>
|
|
|
|
|
|
/// <returns>
|
|
|
|
|
|
/// Returns the current back office identity if an admin is authenticated otherwise null
|
|
|
|
|
|
/// </returns>
|
2020-06-02 13:28:30 +10:00
|
|
|
|
public static UmbracoBackOfficeIdentity GetCurrentIdentity(this HttpContextBase http)
|
2018-04-05 23:10:51 +10:00
|
|
|
|
{
|
|
|
|
|
|
if (http == null) throw new ArgumentNullException(nameof(http));
|
|
|
|
|
|
if (http.User == null) return null; //there's no user at all so no identity
|
|
|
|
|
|
|
2019-01-26 10:52:19 -05:00
|
|
|
|
// If it's already a UmbracoBackOfficeIdentity
|
2018-04-05 23:10:51 +10:00
|
|
|
|
var backOfficeIdentity = http.User.GetUmbracoIdentity();
|
|
|
|
|
|
if (backOfficeIdentity != null) return backOfficeIdentity;
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// This will return the current back office identity.
|
|
|
|
|
|
/// </summary>
|
2020-06-02 13:28:30 +10:00
|
|
|
|
/// <param name="http"></param>
|
2018-04-05 23:10:51 +10:00
|
|
|
|
/// <returns>
|
|
|
|
|
|
/// Returns the current back office identity if an admin is authenticated otherwise null
|
|
|
|
|
|
/// </returns>
|
2020-06-02 13:28:30 +10:00
|
|
|
|
internal static UmbracoBackOfficeIdentity GetCurrentIdentity(this HttpContext http)
|
2018-04-05 23:10:51 +10:00
|
|
|
|
{
|
|
|
|
|
|
if (http == null) throw new ArgumentNullException("http");
|
2020-06-02 13:28:30 +10:00
|
|
|
|
return new HttpContextWrapper(http).GetCurrentIdentity();
|
2018-04-05 23:10:51 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-06-03 12:47:40 +10:00
|
|
|
|
// NOTE: Migrated to netcore (though in a different way)
|
2018-04-05 23:10:51 +10:00
|
|
|
|
public static double GetRemainingAuthSeconds(this HttpContextBase http)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (http == null) throw new ArgumentNullException(nameof(http));
|
|
|
|
|
|
var ticket = http.GetUmbracoAuthTicket();
|
|
|
|
|
|
return ticket.GetRemainingAuthSeconds();
|
|
|
|
|
|
}
|
2018-12-12 17:49:24 +01:00
|
|
|
|
|
2020-06-03 12:47:40 +10:00
|
|
|
|
// NOTE: Migrated to netcore (though in a different way)
|
2018-04-05 23:10:51 +10:00
|
|
|
|
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));
|
2020-03-12 14:36:25 +01:00
|
|
|
|
return GetAuthTicket(http, Current.Configs.Security().AuthCookieName);
|
2018-04-05 23:10:51 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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));
|
2020-03-12 14:36:25 +01:00
|
|
|
|
return GetAuthTicket(ctx, Current.Configs.Security().AuthCookieName);
|
2018-04-05 23:10:51 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
2019-01-26 10:52:19 -05:00
|
|
|
|
// get the ticket
|
2018-04-05 23:10:51 +10:00
|
|
|
|
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();
|
|
|
|
|
|
|
2019-01-26 10:52:19 -05:00
|
|
|
|
// will only happen in tests
|
2018-04-06 13:51:54 +10:00
|
|
|
|
if (secureFormat == null) return null;
|
|
|
|
|
|
|
2019-01-26 10:52:19 -05:00
|
|
|
|
// get the ticket
|
2018-04-05 23:10:51 +10:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
return GetAuthTicket(secureFormat, asDictionary, cookieName);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception)
|
|
|
|
|
|
{
|
2019-01-26 10:52:19 -05:00
|
|
|
|
// occurs when decryption fails
|
2020-06-02 13:28:30 +10:00
|
|
|
|
|
2018-04-05 23:10:51 +10:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2019-01-26 10:52:19 -05:00
|
|
|
|
// get the ticket
|
2018-12-12 17:49:24 +01:00
|
|
|
|
|
2018-04-05 23:10:51 +10:00
|
|
|
|
return secureDataFormat.Unprotect(formsCookie);
|
|
|
|
|
|
}
|
2020-01-07 13:50:38 +01:00
|
|
|
|
|
2020-06-02 13:28:30 +10:00
|
|
|
|
|
2018-04-05 23:10:51 +10:00
|
|
|
|
}
|
|
|
|
|
|
}
|