Repository navigation
feat(session): serve session expiry from VIPER 2, not the legacy CFM #304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rlorenzo
wants to merge
1
commit into
fix/session-refresh-pathbase
from
feature/session-timeout-endpoint
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| using System.Globalization; | ||
| using Viper.Classes.Utilities; | ||
| using Viper.Models.VIPER; | ||
|
|
||
| namespace Viper.test.Classes | ||
| { | ||
| public class SessionTimeoutServiceTests | ||
| { | ||
| [Fact] | ||
| public void ToStatus_StoredRow_ReportsThatExpiryWithLocalOffset() | ||
| { | ||
| DateTime expiry = DateTime.Now.AddMinutes(10); | ||
|
|
||
| var status = SessionTimeoutService.ToStatus(new SessionTimeout { SessionTimeoutDateTime = expiry }, authenticated: true); | ||
|
|
||
| Assert.InRange(status.SecondsUntilTimeout, 590, 600); | ||
| var parsed = DateTimeOffset.ParseExact(status.SessionTimeoutDateTime, "yyyy-MM-dd'T'HH:mm:sszzz", CultureInfo.InvariantCulture); | ||
| Assert.Equal(expiry.ToString("s"), parsed.DateTime.ToString("s")); | ||
| Assert.Equal(TimeZoneInfo.Local.GetUtcOffset(expiry), parsed.Offset); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ToStatus_AuthenticatedWithoutRow_GrantsFullWindow() | ||
| { | ||
| var status = SessionTimeoutService.ToStatus(null, authenticated: true); | ||
|
|
||
| Assert.Equal(SessionTimeoutService.SessionTimeoutSeconds, status.SecondsUntilTimeout); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ToStatus_Anonymous_ReportsExpired() | ||
| { | ||
| Assert.Equal(0, SessionTimeoutService.ToStatus(null, authenticated: false).SecondsUntilTimeout); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| using System.Reflection; | ||
| using System.Security.Claims; | ||
| using Microsoft.AspNetCore.Authentication; | ||
| using Microsoft.AspNetCore.Authentication.Cookies; | ||
| using Microsoft.AspNetCore.Http; | ||
| using Microsoft.AspNetCore.Mvc.Controllers; | ||
| using Viper.Classes; | ||
| using Viper.Controllers; | ||
|
|
||
| namespace Viper.test.Controllers | ||
| { | ||
| /// <summary> | ||
| /// The session expiry poll must never extend the session, or it would never time out. Both | ||
| /// controller bases in this app write a fresh expiry on every action, so the guarantee rests | ||
| /// entirely on this controller not inheriting either of them. Pin that. | ||
| /// </summary> | ||
| public class SessionTimeoutControllerTests | ||
| { | ||
| [Fact] | ||
| public void Controller_DoesNotInheritASessionExtendingBase() | ||
| { | ||
| Assert.False(typeof(ApiController).IsAssignableFrom(typeof(SessionTimeoutController))); | ||
| Assert.False(typeof(AreaController).IsAssignableFrom(typeof(SessionTimeoutController))); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Controller_DoesNotCarryTheSessionUpdateFilter() | ||
| { | ||
| Assert.Empty(typeof(SessionTimeoutController) | ||
| .GetCustomAttributes(typeof(ApiSessionUpdateFilterAttribute), inherit: true)); | ||
|
|
||
| foreach (MethodInfo action in typeof(SessionTimeoutController).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)) | ||
| { | ||
| Assert.Empty(action.GetCustomAttributes(typeof(ApiSessionUpdateFilterAttribute), inherit: true)); | ||
| } | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData(typeof(SessionTimeoutController), false)] | ||
| [InlineData(typeof(HomeController), true)] | ||
| public async Task DoNotSlideCookie_RenewsOnlyForOtherEndpoints(Type controller, bool expectedRenew) | ||
| { | ||
| var httpContext = new DefaultHttpContext(); | ||
| var descriptor = new ControllerActionDescriptor { ControllerTypeInfo = controller.GetTypeInfo() }; | ||
| httpContext.SetEndpoint(new Endpoint(null, new EndpointMetadataCollection(descriptor), controller.Name)); | ||
| var ticket = new AuthenticationTicket(new ClaimsPrincipal(), CookieAuthenticationDefaults.AuthenticationScheme); | ||
| var context = new CookieSlidingExpirationContext(httpContext, | ||
| new AuthenticationScheme(CookieAuthenticationDefaults.AuthenticationScheme, null, typeof(CookieAuthenticationHandler)), | ||
| new CookieAuthenticationOptions(), ticket, TimeSpan.FromHours(7), TimeSpan.FromHours(5)) | ||
| { ShouldRenew = true }; | ||
|
|
||
| await SessionTimeoutController.DoNotSlideCookie(context); | ||
|
|
||
| Assert.Equal(expectedRenew, context.ShouldRenew); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| using Microsoft.AspNetCore.Authentication.Cookies; | ||
| using Microsoft.AspNetCore.Authorization; | ||
| using Microsoft.AspNetCore.Mvc; | ||
| using Microsoft.AspNetCore.Mvc.Controllers; | ||
| using Viper.Classes.SQLContext; | ||
| using Viper.Classes.Utilities; | ||
| using Viper.Models; | ||
|
|
||
| namespace Viper.Controllers | ||
| { | ||
| /// <summary> | ||
| /// Read-only session expiry poll for the session timeout dialog. The user comes from the auth cookie. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Inherits ControllerBase, not Viper.Classes.ApiController or AreaController: both extend the session on | ||
| /// every action. The [ApiController] attribute below is MVC's ApiControllerAttribute and does not. | ||
| /// [AllowAnonymous] so a caller whose cookie is gone gets 0 seconds and a Log in button, not a challenge redirect. | ||
| /// </remarks> | ||
| [ApiController] | ||
|
rlorenzo marked this conversation as resolved.
Resolved
|
||
| [AllowAnonymous] | ||
| [Route("/api/sessionTimeout")] | ||
| [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] | ||
| public class SessionTimeoutController : ControllerBase | ||
|
rlorenzo marked this conversation as resolved.
Resolved
|
||
| { | ||
| private readonly VIPERContext _viperContext; | ||
|
|
||
| public SessionTimeoutController(VIPERContext viperContext) | ||
| { | ||
| _viperContext = viperContext; | ||
| } | ||
|
|
||
| // A background read must not slide the auth cookie either, or a tab left open on a page with no | ||
| // session row would poll forever and keep the login alive past its 12 hours. | ||
| internal static Task DoNotSlideCookie(CookieSlidingExpirationContext context) | ||
| { | ||
| var action = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<ControllerActionDescriptor>(); | ||
| if (action?.ControllerTypeInfo == typeof(SessionTimeoutController)) | ||
| { | ||
| context.ShouldRenew = false; | ||
| } | ||
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| [HttpGet] | ||
| public SessionTimeoutStatus GetSessionTimeout() | ||
| { | ||
| // No catch: a database failure becomes a 500, which the client treats as a failed poll and retries. | ||
| // A made-up expiry here would read as a real one and could hide a live warning. | ||
| return SessionTimeoutService.GetStatus(_viperContext); | ||
| } | ||
| } | ||
| } | ||
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| using System.Globalization; | ||
|
|
||
| namespace Viper.Models | ||
| { | ||
| /// <summary> | ||
| /// Session expiry contract polled by the session timeout dialog. | ||
| /// </summary> | ||
| public class SessionTimeoutStatus | ||
| { | ||
| public SessionTimeoutStatus(DateTime sessionTimeout, int secondsUntilTimeout) | ||
| { | ||
| // The column is a bare local datetime; carry the offset so a client in another timezone shows the right time. | ||
| DateTimeOffset local = new(DateTime.SpecifyKind(sessionTimeout, DateTimeKind.Local)); | ||
| SessionTimeoutDateTime = local.ToString("yyyy-MM-dd'T'HH:mm:sszzz", CultureInfo.InvariantCulture); | ||
| SecondsUntilTimeout = secondsUntilTimeout; | ||
| } | ||
|
|
||
| public string SessionTimeoutDateTime { get; } | ||
|
|
||
| public int SecondsUntilTimeout { get; } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.