2020-06-02 13:28:30 +10:00
using Microsoft.AspNetCore.Authentication ;
using Microsoft.AspNetCore.Authentication.Cookies ;
using Microsoft.AspNetCore.Http ;
using Microsoft.AspNetCore.Http.Extensions ;
using System ;
using System.Collections.Generic ;
using System.Security.Claims ;
using System.Text ;
using System.Threading.Tasks ;
using Umbraco.Core ;
using Umbraco.Core.BackOffice ;
using Umbraco.Core.Configuration ;
using Umbraco.Core.Hosting ;
using Umbraco.Extensions ;
namespace Umbraco.Web.BackOffice.Security
{
using ICookieManager = Microsoft . AspNetCore . Authentication . Cookies . ICookieManager ;
/// <summary>
2020-06-02 14:51:27 +10:00
/// Used to validate a cookie against a user's session id
2020-06-02 13:28:30 +10:00
/// </summary>
/// <remarks>
2020-06-02 14:51:27 +10:00
/// <para>
2020-06-02 13:28:30 +10:00
/// This uses another cookie to track the last checked time which is done for a few reasons:
/// * We can't use the user's auth ticket to do this because we'd be re-issuing the auth ticket all of the time and it would never expire
/// plus the auth ticket size is much larger than this small value
/// * This will execute quite often (every minute per user) and in some cases there might be several requests that end up re-issuing the cookie so the cookie value should be small
/// * We want to avoid the user lookup if it's not required so that will only happen when the time diff is great enough in the cookie
2020-06-02 14:51:27 +10:00
/// </para>
/// <para>
/// This is a scoped/request based object.
/// </para>
2020-06-02 13:28:30 +10:00
/// </remarks>
public class BackOfficeSessionIdValidator
{
public const string CookieName = "UMB_UCONTEXT_C" ;
private readonly ISystemClock _systemClock ;
private readonly IGlobalSettings _globalSettings ;
private readonly IHostingEnvironment _hostingEnvironment ;
private readonly BackOfficeUserManager _userManager ;
public BackOfficeSessionIdValidator ( ISystemClock systemClock , IGlobalSettings globalSettings , IHostingEnvironment hostingEnvironment , BackOfficeUserManager userManager )
{
_systemClock = systemClock ;
_globalSettings = globalSettings ;
_hostingEnvironment = hostingEnvironment ;
_userManager = userManager ;
}
public async Task ValidateSessionAsync ( TimeSpan validateInterval , CookieValidatePrincipalContext context )
{
if ( ! context . Request . IsBackOfficeRequest ( _globalSettings , _hostingEnvironment ) )
return ;
var valid = await ValidateSessionAsync ( validateInterval , context . HttpContext , context . Options . CookieManager , _systemClock , context . Properties . IssuedUtc , context . Principal . Identity as ClaimsIdentity ) ;
if ( valid = = false )
{
context . RejectPrincipal ( ) ;
await context . HttpContext . SignOutAsync ( Constants . Security . BackOfficeAuthenticationType ) ;
}
}
private async Task < bool > ValidateSessionAsync (
TimeSpan validateInterval ,
HttpContext httpContext ,
ICookieManager cookieManager ,
ISystemClock systemClock ,
DateTimeOffset ? authTicketIssueDate ,
ClaimsIdentity currentIdentity )
{
if ( httpContext = = null ) throw new ArgumentNullException ( nameof ( httpContext ) ) ;
if ( cookieManager = = null ) throw new ArgumentNullException ( nameof ( cookieManager ) ) ;
if ( systemClock = = null ) throw new ArgumentNullException ( nameof ( systemClock ) ) ;
if ( currentIdentity = = null )
{
return false ;
}
DateTimeOffset ? issuedUtc = null ;
var currentUtc = systemClock . UtcNow ;
//read the last checked time from a custom cookie
var lastCheckedCookie = cookieManager . GetRequestCookie ( httpContext , CookieName ) ;
if ( lastCheckedCookie . IsNullOrWhiteSpace ( ) = = false )
{
if ( DateTimeOffset . TryParse ( lastCheckedCookie , out var parsed ) )
{
issuedUtc = parsed ;
}
}
//no cookie, use the issue time of the auth ticket
if ( issuedUtc . HasValue = = false )
{
issuedUtc = authTicketIssueDate ;
}
// Only validate if enough time has elapsed
var validate = issuedUtc . HasValue = = false ;
if ( issuedUtc . HasValue )
{
var timeElapsed = currentUtc . Subtract ( issuedUtc . Value ) ;
validate = timeElapsed > validateInterval ;
}
if ( validate = = false )
return true ;
var userId = currentIdentity . GetUserId ( ) ;
var user = await _userManager . FindByIdAsync ( userId ) ;
if ( user = = null )
return false ;
var sessionId = currentIdentity . FindFirstValue ( Constants . Security . SessionIdClaimType ) ;
if ( await _userManager . ValidateSessionIdAsync ( userId , sessionId ) = = false )
return false ;
//we will re-issue the cookie last checked cookie
cookieManager . AppendResponseCookie (
httpContext ,
CookieName ,
DateTimeOffset . UtcNow . ToString ( "yyyy-MM-ddTHH:mm:ss.fffffffzzz" ) ,
new CookieOptions
{
HttpOnly = true ,
Secure = _globalSettings . UseHttps | | httpContext . Request . IsHttps ,
Path = "/"
} ) ;
return true ;
}
}
}