Skip to content

Commit 4f0a837

Browse files
nikolajlauridsennikolajlauridsenbergmania
authored
V9: Fix login timeout (#12029)
* Turn SlidingExpiration off and only renew cookie of not RemainingSeconds request Also adds the TicketExpiresClaim before validating the the security stamp, otherwise the claim won't be merged and "dissappear", leading to the user being instantly logged out Also only EnsureValidSessionId if not RemainingSeconds request, otherwise the session will always be valid, since the remaining seconds request renews it. * Don't ignore SessionIdClaimType and Cookiepath when merging claims Besides what the comment used to state these claims are only issued when logging in, leading you to be logged out once the claims are merged, furthermore when we check the session ID we verify that you session has not expired. * Manually specify Issued and Expires when renewing token If we don't we lose 30 minutes of our ExpireTimeSpan every time the principal refreshes * Re-add ignored claims And use MergeAllClaims on refreshing principal instead. * EnsureValidSessionId before updating IssuedUtc * Fix comment * Update src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs Co-authored-by: nikolajlauridsen <[email protected]> Co-authored-by: Bjarke Berg <[email protected]>
1 parent ab87034 commit 4f0a837

File tree

4 files changed

+47
-7
lines changed

4 files changed

+47
-7
lines changed

src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ public static bool VerifyBackOfficeIdentity(this ClaimsIdentity identity, out Cl
132132
}
133133
}
134134

135-
verifiedIdentity = identity.AuthenticationType == Constants.Security.BackOfficeAuthenticationType ? identity : new ClaimsIdentity(identity.Claims, Constants.Security.BackOfficeAuthenticationType);
135+
verifiedIdentity = identity.AuthenticationType == Constants.Security.BackOfficeAuthenticationType ? identity : new ClaimsIdentity(identity.Claims, Constants.Security.BackOfficeAuthenticationType);
136136
return true;
137137
}
138138

src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) Umbraco.
22
// See LICENSE for more details.
3+
34
using System.Linq;
45
using System.Security.Claims;
56
using Umbraco.Cms.Core;
@@ -11,7 +12,8 @@ public static class MergeClaimsIdentityExtensions
1112
{
1213
// Ignore these Claims when merging, these claims are dynamically added whenever the ticket
1314
// is re-issued and we don't want to merge old values of these.
14-
private static readonly string[] s_ignoredClaims = new[] { ClaimTypes.CookiePath, Constants.Security.SessionIdClaimType };
15+
// We do however want to merge these when the SecurityStampValidator refreshes the principal since it's still the same login session
16+
private static readonly string[] s_ignoredClaims = { ClaimTypes.CookiePath, Constants.Security.SessionIdClaimType };
1517

1618
public static void MergeAllClaims(this ClaimsIdentity destination, ClaimsIdentity source)
1719
{

src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using Umbraco.Cms.Core.Routing;
1616
using Umbraco.Cms.Core.Services;
1717
using Umbraco.Cms.Core.Web;
18+
using Umbraco.Cms.Web.BackOffice.Controllers;
1819
using Umbraco.Extensions;
1920

2021
namespace Umbraco.Cms.Web.BackOffice.Security
@@ -92,7 +93,7 @@ public void Configure(string name, CookieAuthenticationOptions options)
9293
/// <inheritdoc />
9394
public void Configure(CookieAuthenticationOptions options)
9495
{
95-
options.SlidingExpiration = true;
96+
options.SlidingExpiration = false;
9697
options.ExpireTimeSpan = _globalSettings.TimeOut;
9798
options.Cookie.Domain = _securitySettings.AuthCookieDomain;
9899
options.Cookie.Name = _securitySettings.AuthCookieName;
@@ -150,8 +151,6 @@ public void Configure(CookieAuthenticationOptions options)
150151
// ensure the thread culture is set
151152
backOfficeIdentity.EnsureCulture();
152153

153-
await EnsureValidSessionId(ctx);
154-
await securityStampValidator.ValidateAsync(ctx);
155154
EnsureTicketRenewalIfKeepUserLoggedIn(ctx);
156155

157156
// add or update a claim to track when the cookie expires, we use this to track time remaining
@@ -163,6 +162,28 @@ public void Configure(CookieAuthenticationOptions options)
163162
Constants.Security.BackOfficeAuthenticationType,
164163
backOfficeIdentity));
165164

165+
await securityStampValidator.ValidateAsync(ctx);
166+
167+
// This might have been called from GetRemainingTimeoutSeconds, in this case we don't want to ensure valid session
168+
// since that in it self will keep the session valid since we renew the lastVerified date.
169+
// Similarly don't renew the token
170+
if (IsRemainingSecondsRequest(ctx))
171+
{
172+
return;
173+
}
174+
175+
// This relies on IssuedUtc, so call it before updating it.
176+
await EnsureValidSessionId(ctx);
177+
178+
// We have to manually specify Issued and Expires,
179+
// because the SecurityStampValidator refreshes the principal every 30 minutes,
180+
// When the principal is refreshed the Issued is update to time of refresh, however, the Expires remains unchanged
181+
// When we then try and renew, the difference of issued and expires effectively becomes the new ExpireTimeSpan
182+
// meaning we effectively lose 30 minutes of our ExpireTimeSpan for EVERY principal refresh if we don't
183+
// https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authentication/Cookies/src/CookieAuthenticationHandler.cs#L115
184+
ctx.Properties.IssuedUtc = _systemClock.UtcNow;
185+
ctx.Properties.ExpiresUtc = _systemClock.UtcNow.Add(_globalSettings.TimeOut);
186+
ctx.ShouldRenew = true;
166187
},
167188
OnSigningIn = ctx =>
168189
{
@@ -226,7 +247,7 @@ public void Configure(CookieAuthenticationOptions options)
226247
}
227248

228249
return Task.CompletedTask;
229-
}
250+
},
230251
};
231252
}
232253

@@ -276,5 +297,21 @@ private void EnsureTicketRenewalIfKeepUserLoggedIn(CookieValidatePrincipalContex
276297
}
277298
}
278299
}
300+
301+
private bool IsRemainingSecondsRequest(CookieValidatePrincipalContext context)
302+
{
303+
var routeValues = context.HttpContext.Request.RouteValues;
304+
if (routeValues.TryGetValue("controller", out var controllerName) &&
305+
routeValues.TryGetValue("action", out var action))
306+
{
307+
if (controllerName?.ToString() == ControllerExtensions.GetControllerName<AuthenticationController>()
308+
&& action?.ToString() == nameof(AuthenticationController.GetRemainingTimeoutSeconds))
309+
{
310+
return true;
311+
}
312+
}
313+
314+
return false;
315+
}
279316
}
280317
}

src/Umbraco.Web.Common/Security/ConfigureSecurityStampOptions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ public static void ConfigureOptions(SecurityStampValidatorOptions options)
3030
ClaimsIdentity newIdentity = refreshingPrincipal.NewPrincipal.Identities.First();
3131
ClaimsIdentity currentIdentity = refreshingPrincipal.CurrentPrincipal.Identities.First();
3232

33-
newIdentity.MergeClaimsFromCookieIdentity(currentIdentity);
33+
// Since this is refreshing an existing principal, we want to merge all claims.
34+
newIdentity.MergeAllClaims(currentIdentity);
3435

3536
return Task.CompletedTask;
3637
};

0 commit comments

Comments
 (0)