mirror of https://github.com/abpframework/abp.git
21 changed files with 755 additions and 17 deletions
@ -0,0 +1,104 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Security.Claims; |
|||
using System.Security.Principal; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Authentication; |
|||
using Microsoft.AspNetCore.Authentication.Cookies; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.AspNetCore.Http; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.Security.Claims; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.Client; |
|||
|
|||
public static class AbpRemoteDynamicClaimsCookieAuthenticationOptionsExtensions |
|||
{ |
|||
public static CookieAuthenticationOptions ValidateRemoteDynamicClaims(this CookieAuthenticationOptions options) |
|||
{ |
|||
var previousOnCheckSlidingExpiration = options.Events.OnCheckSlidingExpiration; |
|||
options.Events.OnCheckSlidingExpiration = async cookieSlidingExpirationContext => |
|||
{ |
|||
await previousOnCheckSlidingExpiration(cookieSlidingExpirationContext); |
|||
|
|||
if (cookieSlidingExpirationContext.ShouldRenew && |
|||
cookieSlidingExpirationContext.Principal != null && |
|||
!await AreRemoteDynamicClaimsValidAsync(cookieSlidingExpirationContext.HttpContext, cookieSlidingExpirationContext.Scheme.Name, cookieSlidingExpirationContext.Principal, cookieSlidingExpirationContext.Properties)) |
|||
{ |
|||
cookieSlidingExpirationContext.ShouldRenew = false; |
|||
} |
|||
}; |
|||
|
|||
var previousOnValidatePrincipal = options.Events.OnValidatePrincipal; |
|||
options.Events.OnValidatePrincipal = async cookieValidatePrincipalContext => |
|||
{ |
|||
await previousOnValidatePrincipal(cookieValidatePrincipalContext); |
|||
|
|||
if (cookieValidatePrincipalContext.Principal != null && |
|||
!await AreRemoteDynamicClaimsValidAsync(cookieValidatePrincipalContext.HttpContext, cookieValidatePrincipalContext.Scheme.Name, cookieValidatePrincipalContext.Principal, cookieValidatePrincipalContext.Properties)) |
|||
{ |
|||
cookieValidatePrincipalContext.ShouldRenew = false; |
|||
cookieValidatePrincipalContext.RejectPrincipal(); |
|||
} |
|||
}; |
|||
|
|||
return options; |
|||
} |
|||
|
|||
private static async Task<bool> AreRemoteDynamicClaimsValidAsync(HttpContext httpContext, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) |
|||
{ |
|||
if (scheme == IdentityConstants.ExternalScheme || |
|||
scheme == IdentityConstants.TwoFactorUserIdScheme || |
|||
scheme == IdentityConstants.TwoFactorRememberMeScheme) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
var identity = principal.Identities.FirstOrDefault(); |
|||
var userId = identity?.FindUserId(); |
|||
if (userId == null) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
var abpClaimsPrincipalFactoryOptions = httpContext.RequestServices.GetRequiredService<IOptions<AbpClaimsPrincipalFactoryOptions>>().Value; |
|||
if (!abpClaimsPrincipalFactoryOptions.IsDynamicClaimsEnabled || !abpClaimsPrincipalFactoryOptions.IsRemoteRefreshEnabled) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
var accessToken = properties.GetTokenValue("access_token"); |
|||
if (accessToken.IsNullOrWhiteSpace()) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
var dynamicClaimsCache = httpContext.RequestServices.GetService<RemoteDynamicClaimsPrincipalContributorCache>(); |
|||
if (dynamicClaimsCache == null) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
// The multi-tenancy middleware hasn't resolved the tenant yet, but the dynamic claims are cached per tenant.
|
|||
var tenantId = identity!.FindTenantId(); |
|||
using (httpContext.RequestServices.GetRequiredService<ICurrentTenant>().Change(tenantId)) |
|||
{ |
|||
await dynamicClaimsCache.GetAsync(userId.Value, tenantId, accessToken); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
httpContext.RequestServices |
|||
.GetRequiredService<ILogger<AbpAspNetCoreMvcClientModule>>() |
|||
.LogWarning(e, $"Failed to refresh remote dynamic claims for user: {userId.Value}, the authentication cookie is rejected."); |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,109 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Net; |
|||
using System.Net.Http; |
|||
using System.Security.Claims; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Authentication; |
|||
using Microsoft.AspNetCore.Authentication.Cookies; |
|||
using Microsoft.AspNetCore.Http; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.DependencyInjection.Extensions; |
|||
using NSubstitute; |
|||
using Shouldly; |
|||
using Volo.Abp.Caching; |
|||
using Volo.Abp.Http.Client.Authentication; |
|||
using Volo.Abp.Security.Claims; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.Client; |
|||
|
|||
public class AbpRemoteDynamicClaimsCookieAuthenticationOptionsExtensions_Tests : AbpAspNetCoreMvcClientTestBase |
|||
{ |
|||
private readonly RemoteRefreshResponseHandler _remoteRefreshResponseHandler = new(); |
|||
|
|||
protected override void AfterAddApplication(IServiceCollection services) |
|||
{ |
|||
var httpClientFactory = Substitute.For<IHttpClientFactory>(); |
|||
httpClientFactory.CreateClient(Arg.Any<string>()).Returns(_ => new HttpClient(_remoteRefreshResponseHandler) |
|||
{ |
|||
BaseAddress = new Uri("https://localhost/") |
|||
}); |
|||
|
|||
services.Replace(ServiceDescriptor.Singleton(httpClientFactory)); |
|||
services.Replace(ServiceDescriptor.Transient(_ => Substitute.For<IRemoteServiceHttpClientAuthenticator>())); |
|||
services.Configure<AbpClaimsPrincipalFactoryOptions>(options => |
|||
{ |
|||
options.IsDynamicClaimsEnabled = true; |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Reject_The_Principal_When_The_Remote_Refresh_Is_Rejected() |
|||
{ |
|||
var context = await ValidatePrincipalAsync(Guid.NewGuid()); |
|||
|
|||
context.Principal.ShouldBeNull(); |
|||
context.ShouldRenew.ShouldBeFalse(); |
|||
_remoteRefreshResponseHandler.ReceivedAccessTokens.ShouldBe(new[] { "test-access-token" }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Keep_The_Principal_When_The_Dynamic_Claims_Are_Cached() |
|||
{ |
|||
var userId = Guid.NewGuid(); |
|||
await GetRequiredService<IDistributedCache<AbpDynamicClaimCacheItem>>().SetAsync( |
|||
AbpDynamicClaimCacheItem.CalculateCacheKey(userId, null), |
|||
new AbpDynamicClaimCacheItem()); |
|||
|
|||
var context = await ValidatePrincipalAsync(userId); |
|||
|
|||
context.Principal.ShouldNotBeNull(); |
|||
_remoteRefreshResponseHandler.ReceivedAccessTokens.ShouldBeEmpty(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Keep_The_Principal_When_There_Is_No_Access_Token() |
|||
{ |
|||
var context = await ValidatePrincipalAsync(Guid.NewGuid(), accessToken: null); |
|||
|
|||
context.Principal.ShouldNotBeNull(); |
|||
_remoteRefreshResponseHandler.ReceivedAccessTokens.ShouldBeEmpty(); |
|||
} |
|||
|
|||
private async Task<CookieValidatePrincipalContext> ValidatePrincipalAsync(Guid userId, string accessToken = "test-access-token") |
|||
{ |
|||
var options = new CookieAuthenticationOptions().ValidateRemoteDynamicClaims(); |
|||
|
|||
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme, AbpClaimTypes.UserName, AbpClaimTypes.Role); |
|||
identity.AddClaim(new Claim(AbpClaimTypes.UserId, userId.ToString())); |
|||
identity.AddClaim(new Claim(AbpClaimTypes.UserName, "john")); |
|||
|
|||
var properties = new AuthenticationProperties(); |
|||
if (accessToken != null) |
|||
{ |
|||
properties.StoreTokens(new[] { new AuthenticationToken { Name = "access_token", Value = accessToken } }); |
|||
} |
|||
|
|||
var context = new CookieValidatePrincipalContext( |
|||
new DefaultHttpContext { RequestServices = ServiceProvider }, |
|||
new AuthenticationScheme(CookieAuthenticationDefaults.AuthenticationScheme, null, typeof(CookieAuthenticationHandler)), |
|||
options, |
|||
new AuthenticationTicket(new ClaimsPrincipal(identity), properties, CookieAuthenticationDefaults.AuthenticationScheme)); |
|||
|
|||
await options.Events.ValidatePrincipal(context); |
|||
return context; |
|||
} |
|||
|
|||
private class RemoteRefreshResponseHandler : HttpMessageHandler |
|||
{ |
|||
public List<string> ReceivedAccessTokens { get; } = new(); |
|||
|
|||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) |
|||
{ |
|||
ReceivedAccessTokens.Add(request.Headers.Authorization?.Parameter); |
|||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Unauthorized)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
using System; |
|||
using System.Security.Claims; |
|||
using System.Security.Principal; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Authentication.Cookies; |
|||
using Microsoft.AspNetCore.Http; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.Security.Claims; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
public static class AbpIdentitySessionCookieAuthenticationOptionsExtensions |
|||
{ |
|||
public static CookieAuthenticationOptions ValidateIdentitySession(this CookieAuthenticationOptions options) |
|||
{ |
|||
var previousOnCheckSlidingExpiration = options.Events.OnCheckSlidingExpiration; |
|||
options.Events.OnCheckSlidingExpiration = async cookieSlidingExpirationContext => |
|||
{ |
|||
await previousOnCheckSlidingExpiration(cookieSlidingExpirationContext); |
|||
|
|||
if (cookieSlidingExpirationContext.ShouldRenew && |
|||
!await IsIdentitySessionValidAsync(cookieSlidingExpirationContext.HttpContext, cookieSlidingExpirationContext.Principal)) |
|||
{ |
|||
cookieSlidingExpirationContext.ShouldRenew = false; |
|||
} |
|||
}; |
|||
|
|||
var previousOnValidatePrincipal = options.Events.OnValidatePrincipal; |
|||
options.Events.OnValidatePrincipal = async cookieValidatePrincipalContext => |
|||
{ |
|||
await previousOnValidatePrincipal(cookieValidatePrincipalContext); |
|||
|
|||
if (cookieValidatePrincipalContext.Principal != null && |
|||
!await IsIdentitySessionValidAsync(cookieValidatePrincipalContext.HttpContext, cookieValidatePrincipalContext.Principal)) |
|||
{ |
|||
cookieValidatePrincipalContext.ShouldRenew = false; |
|||
cookieValidatePrincipalContext.RejectPrincipal(); |
|||
} |
|||
}; |
|||
|
|||
return options; |
|||
} |
|||
|
|||
private static async Task<bool> IsIdentitySessionValidAsync(HttpContext httpContext, ClaimsPrincipal principal) |
|||
{ |
|||
var sessionId = principal.FindSessionId(); |
|||
if (sessionId.IsNullOrWhiteSpace()) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
if (!httpContext.RequestServices.GetRequiredService<IOptions<AbpClaimsPrincipalFactoryOptions>>().Value.IsDynamicClaimsEnabled) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
var currentTenant = httpContext.RequestServices.GetRequiredService<ICurrentTenant>(); |
|||
var identitySessionChecker = httpContext.RequestServices.GetRequiredService<IIdentitySessionChecker>(); |
|||
using (currentTenant.Change(principal.FindTenantId())) |
|||
{ |
|||
return await identitySessionChecker.IsValidateAsync(sessionId); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
using System.Collections.Generic; |
|||
using Microsoft.AspNetCore.Http; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
public class HttpContextIdentitySessionValidationResultAccessor : IIdentitySessionValidationResultAccessor, ITransientDependency |
|||
{ |
|||
public const string HttpContextItemName = "__AbpIdentitySessionValidationResults"; |
|||
|
|||
private readonly IHttpContextAccessor _httpContextAccessor; |
|||
|
|||
public HttpContextIdentitySessionValidationResultAccessor(IHttpContextAccessor httpContextAccessor) |
|||
{ |
|||
_httpContextAccessor = httpContextAccessor; |
|||
} |
|||
|
|||
public bool? GetOrNull(string sessionId) |
|||
{ |
|||
return GetResults()?.TryGetValue(sessionId, out var isValid) == true ? isValid : null; |
|||
} |
|||
|
|||
public void Set(string sessionId, bool isValid) |
|||
{ |
|||
var httpContext = _httpContextAccessor.HttpContext; |
|||
if (httpContext == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var results = GetResults(); |
|||
if (results == null) |
|||
{ |
|||
results = new Dictionary<string, bool>(); |
|||
httpContext.Items[HttpContextItemName] = results; |
|||
} |
|||
|
|||
results[sessionId] = isValid; |
|||
} |
|||
|
|||
private Dictionary<string, bool> GetResults() |
|||
{ |
|||
return _httpContextAccessor.HttpContext?.Items[HttpContextItemName] as Dictionary<string, bool>; |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.Identity; |
|||
|
|||
public interface IIdentitySessionChecker |
|||
{ |
|||
Task<bool> IsValidateAsync(string sessionId); |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
namespace Volo.Abp.Identity; |
|||
|
|||
public interface IIdentitySessionValidationResultAccessor |
|||
{ |
|||
bool? GetOrNull(string sessionId); |
|||
|
|||
void Set(string sessionId, bool isValid); |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Identity; |
|||
|
|||
public class NullIdentitySessionChecker : IIdentitySessionChecker, ISingletonDependency |
|||
{ |
|||
public Task<bool> IsValidateAsync(string sessionId) |
|||
{ |
|||
return Task.FromResult(true); |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Identity; |
|||
|
|||
public class NullIdentitySessionValidationResultAccessor : IIdentitySessionValidationResultAccessor, ISingletonDependency |
|||
{ |
|||
public static NullIdentitySessionValidationResultAccessor Instance { get; } = new(); |
|||
|
|||
public bool? GetOrNull(string sessionId) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
public void Set(string sessionId, bool isValid) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Security.Claims; |
|||
using System.Security.Principal; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Security.Claims; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
public class FakeIdentitySessionChecker : IIdentitySessionChecker, ISingletonDependency |
|||
{ |
|||
public HashSet<string> RevokedSessionIds { get; } = new(); |
|||
|
|||
public Task<bool> IsValidateAsync(string sessionId) |
|||
{ |
|||
return Task.FromResult(!RevokedSessionIds.Contains(sessionId)); |
|||
} |
|||
} |
|||
|
|||
public class TestSessionIdClaimsPrincipalContributor : IAbpClaimsPrincipalContributor, ITransientDependency |
|||
{ |
|||
public Task ContributeAsync(AbpClaimsPrincipalContributorContext context) |
|||
{ |
|||
var identity = context.ClaimsPrincipal.Identities.FirstOrDefault(); |
|||
if (identity != null && identity.FindSessionId() == null) |
|||
{ |
|||
identity.AddClaim(new Claim(AbpClaimTypes.SessionId, Guid.NewGuid().ToString())); |
|||
} |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
using System.Linq; |
|||
using System.Net.Http; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Authentication.Cookies; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Shouldly; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
public class IdentitySessionCookieValidation_Tests : AbpIdentityAspNetCoreTestBase |
|||
{ |
|||
[Fact] |
|||
public async Task Should_Authenticate_The_Cookie_When_The_Session_Is_Valid() |
|||
{ |
|||
var cookie = await LoginAsync(); |
|||
|
|||
using (var response = await GetCurrentUserAsync(cookie)) |
|||
{ |
|||
(await response.Content.ReadAsStringAsync()).ShouldStartWith("admin|"); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Reject_The_Cookie_Without_Writing_It_When_The_Session_Is_Revoked() |
|||
{ |
|||
var cookie = await LoginAsync(); |
|||
var sessionId = await GetSessionIdAsync(cookie); |
|||
|
|||
GetRequiredService<FakeIdentitySessionChecker>().RevokedSessionIds.Add(sessionId); |
|||
|
|||
using (var response = await GetCurrentUserAsync(cookie)) |
|||
{ |
|||
(await response.Content.ReadAsStringAsync()).ShouldBe("anonymous"); |
|||
response.Headers.Contains("Set-Cookie").ShouldBeFalse(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Account_Switch_Cookie_Should_Survive_An_In_Flight_Request_Carrying_The_Old_Revoked_Cookie() |
|||
{ |
|||
var oldCookie = await LoginAsync(); |
|||
var oldSessionId = await GetSessionIdAsync(oldCookie); |
|||
|
|||
var newCookie = await SwitchAccountAsync(oldCookie, "john.nash"); |
|||
newCookie.ShouldNotBeNull(); |
|||
newCookie.ShouldNotBe(oldCookie); |
|||
|
|||
// The switch revoked the old session.
|
|||
GetRequiredService<FakeIdentitySessionChecker>().RevokedSessionIds.Add(oldSessionId); |
|||
|
|||
// An in-flight request still carrying the old cookie must be rejected without emitting a
|
|||
// delete-cookie that would wipe the freshly issued account-switch cookie.
|
|||
using (var response = await GetCurrentUserAsync(oldCookie)) |
|||
{ |
|||
(await response.Content.ReadAsStringAsync()).ShouldBe("anonymous"); |
|||
response.Headers.Contains("Set-Cookie").ShouldBeFalse(); |
|||
} |
|||
|
|||
using (var response = await GetCurrentUserAsync(newCookie)) |
|||
{ |
|||
(await response.Content.ReadAsStringAsync()).ShouldStartWith("john.nash|"); |
|||
} |
|||
} |
|||
|
|||
private async Task<string> SwitchAccountAsync(string cookie, string userName) |
|||
{ |
|||
using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, "api/signin-test/switch-account?userName=" + userName)) |
|||
{ |
|||
requestMessage.Headers.Add("Cookie", cookie); |
|||
using (var response = await Client.SendAsync(requestMessage)) |
|||
{ |
|||
(await response.Content.ReadAsStringAsync()).ShouldBe("Succeeded"); |
|||
var cookieName = CookieAuthenticationDefaults.CookiePrefix + IdentityConstants.ApplicationScheme; |
|||
return response.Headers.GetValues("Set-Cookie") |
|||
.Select(x => x.Split(';')[0]) |
|||
.Last(x => x.StartsWith(cookieName) && x.Length > cookieName.Length + 1); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private async Task<string> LoginAsync() |
|||
{ |
|||
using (var response = await Client.GetAsync("api/signin-test/password?userName=admin&password=1q2w3E*")) |
|||
{ |
|||
(await response.Content.ReadAsStringAsync()).ShouldBe("Succeeded"); |
|||
var cookie = response.Headers.GetValues("Set-Cookie").First(x => x.StartsWith(CookieAuthenticationDefaults.CookiePrefix + IdentityConstants.ApplicationScheme)); |
|||
return cookie.Split(';')[0]; |
|||
} |
|||
} |
|||
|
|||
private async Task<string> GetSessionIdAsync(string cookie) |
|||
{ |
|||
using (var response = await GetCurrentUserAsync(cookie)) |
|||
{ |
|||
return (await response.Content.ReadAsStringAsync()).Split('|')[1]; |
|||
} |
|||
} |
|||
|
|||
private async Task<HttpResponseMessage> GetCurrentUserAsync(string cookie) |
|||
{ |
|||
using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, "api/signin-test/current-user")) |
|||
{ |
|||
requestMessage.Headers.Add("Cookie", cookie); |
|||
return await Client.SendAsync(requestMessage); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,83 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Net.Http; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Authentication.Cookies; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Shouldly; |
|||
using Volo.Abp.AspNetCore.TestBase; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
public class IdentitySessionSlidingRenewal_Tests : AbpAspNetCoreIntegratedTestBase<ShortLivedCookieIdentityTestStartup> |
|||
{ |
|||
private static readonly string AuthenticationCookieName = CookieAuthenticationDefaults.CookiePrefix + IdentityConstants.ApplicationScheme; |
|||
|
|||
[Fact] |
|||
public async Task Should_Renew_The_Cookie_When_The_Session_Is_Valid_Past_The_Half_Life() |
|||
{ |
|||
var cookie = await LoginAsync(); |
|||
|
|||
// Past the half-life of the 10s window, so a renewal is scheduled for a still-valid session.
|
|||
GetRequiredService<TestTimeProvider>().Advance(TimeSpan.FromSeconds(6)); |
|||
|
|||
using (var response = await SendAsync("api/signin-test/current-user", cookie)) |
|||
{ |
|||
(await response.Content.ReadAsStringAsync()).ShouldStartWith("admin|"); |
|||
GetAuthCookieHeader(response).ShouldNotBeNull(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Not_Renew_The_Cookie_When_The_Session_Is_Already_Revoked_At_Auth_Time() |
|||
{ |
|||
var cookie = await LoginAsync(); |
|||
var sessionId = await GetSessionIdAsync(cookie); |
|||
|
|||
GetRequiredService<FakeIdentitySessionChecker>().RevokedSessionIds.Add(sessionId); |
|||
|
|||
// Same instant the valid session would renew at, so the missing renewal is the fix, not expiry.
|
|||
GetRequiredService<TestTimeProvider>().Advance(TimeSpan.FromSeconds(6)); |
|||
|
|||
using (var response = await SendAsync("api/signin-test/current-user", cookie)) |
|||
{ |
|||
(await response.Content.ReadAsStringAsync()).ShouldBe("anonymous"); |
|||
GetAuthCookieHeader(response).ShouldBeNull(); |
|||
} |
|||
} |
|||
|
|||
private async Task<string> LoginAsync() |
|||
{ |
|||
using (var response = await Client.GetAsync("api/signin-test/password?userName=admin&password=1q2w3E*")) |
|||
{ |
|||
(await response.Content.ReadAsStringAsync()).ShouldBe("Succeeded"); |
|||
var cookie = response.Headers.GetValues("Set-Cookie").First(x => x.StartsWith(AuthenticationCookieName)); |
|||
return cookie.Split(';')[0]; |
|||
} |
|||
} |
|||
|
|||
private async Task<string> GetSessionIdAsync(string cookie) |
|||
{ |
|||
using (var response = await SendAsync("api/signin-test/current-user", cookie)) |
|||
{ |
|||
return (await response.Content.ReadAsStringAsync()).Split('|')[1]; |
|||
} |
|||
} |
|||
|
|||
private async Task<HttpResponseMessage> SendAsync(string url, string cookie) |
|||
{ |
|||
using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, url)) |
|||
{ |
|||
requestMessage.Headers.Add("Cookie", cookie); |
|||
return await Client.SendAsync(requestMessage); |
|||
} |
|||
} |
|||
|
|||
private static string GetAuthCookieHeader(HttpResponseMessage response) |
|||
{ |
|||
return response.Headers.TryGetValues("Set-Cookie", out var values) |
|||
? values.FirstOrDefault(x => x.StartsWith(AuthenticationCookieName)) |
|||
: null; |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using System; |
|||
using Microsoft.AspNetCore.Authentication.Cookies; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
[DependsOn(typeof(AbpIdentityAspNetCoreTestModule))] |
|||
public class ShortLivedCookieIdentityTestModule : AbpModule |
|||
{ |
|||
public override void PostConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
var timeProvider = new TestTimeProvider(); |
|||
context.Services.AddSingleton(timeProvider); |
|||
|
|||
// Short lifetime with sliding expiration on a controllable clock, so a request past the
|
|||
// half-life schedules a cookie renewal without real time delays.
|
|||
context.Services.PostConfigure<CookieAuthenticationOptions>(IdentityConstants.ApplicationScheme, options => |
|||
{ |
|||
options.ExpireTimeSpan = TimeSpan.FromSeconds(10); |
|||
options.SlidingExpiration = true; |
|||
options.TimeProvider = timeProvider; |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using Microsoft.AspNetCore.Builder; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Logging; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
public class ShortLivedCookieIdentityTestStartup |
|||
{ |
|||
public void ConfigureServices(IServiceCollection services) |
|||
{ |
|||
services.AddApplication<ShortLivedCookieIdentityTestModule>(); |
|||
} |
|||
|
|||
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) |
|||
{ |
|||
app.InitializeApplication(); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.Identity.AspNetCore; |
|||
|
|||
public class TestTimeProvider : TimeProvider |
|||
{ |
|||
private DateTimeOffset _utcNow = DateTimeOffset.UtcNow; |
|||
|
|||
public override DateTimeOffset GetUtcNow() |
|||
{ |
|||
return _utcNow; |
|||
} |
|||
|
|||
public void Advance(TimeSpan timeSpan) |
|||
{ |
|||
_utcNow = _utcNow.Add(timeSpan); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue