/
githubmirror
/
aspnetcore
Обзор
Документация
Войти
/
githubmirror
/
aspnetcore
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Security/Authentication/test/CookieTests.cs
2 174 строки
89 KB
Stephen Halter
Clear cached session key in cookie auth handler sign-out (#67049)
21 июл 2026, 23:20
Не верифицирован
21 июл 2026, 23:20
15bdfbf
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net; using System.Net.Http; using System.Security.Claims; using System.Security.Principal; using System.Text; using System.Xml.Linq; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Microsoft.Extensions.Time.Testing; using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Authentication.Cookies; public class CookieTests : SharedAuthenticationTests<CookieAuthenticationOptions> { private readonly FakeTimeProvider _timeProvider = new(); protected override string DefaultScheme => CookieAuthenticationDefaults.AuthenticationScheme; protected override Type HandlerType => typeof(CookieAuthenticationHandler); protected override void RegisterAuth(AuthenticationBuilder services, Action<CookieAuthenticationOptions> configure) { services.AddCookie(configure); } [Fact] public void EventsPropertyIsInitializedOnConstruction() { var options = new CookieAuthenticationOptions(); Assert.NotNull(options.Events); } [Fact] public async Task NormalRequestPassesThrough() { using var host = await CreateHost(s => { }); using var server = host.GetTestServer(); var response = await server.CreateClient().GetAsync("http://example.com/normal"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } [Fact] public async Task AjaxLoginRedirectToReturnUrlTurnsInto200WithLocationHeader() { using var host = await CreateHost(o => o.LoginPath = "/login"); using var server = host.GetTestServer(); var transaction = await SendAsync(server, "http://example.com/challenge?X-Requested-With=XMLHttpRequest"); Assert.Equal(HttpStatusCode.Unauthorized, transaction.Response.StatusCode); var responded = transaction.Response.Headers.GetValues("Location"); Assert.Single(responded); Assert.StartsWith("http://example.com/login", responded.Single()); } [Fact] public async Task AjaxForbidTurnsInto403WithLocationHeader() { using var host = await CreateHost(o => o.AccessDeniedPath = "/denied"); using var server = host.GetTestServer(); var transaction = await SendAsync(server, "http://example.com/forbid?X-Requested-With=XMLHttpRequest"); Assert.Equal(HttpStatusCode.Forbidden, transaction.Response.StatusCode); var responded = transaction.Response.Headers.GetValues("Location"); Assert.Single(responded); Assert.StartsWith("http://example.com/denied", responded.Single()); } [Fact] public async Task AjaxLogoutRedirectToReturnUrlTurnsInto200WithLocationHeader() { using var host = await CreateHost(o => o.LogoutPath = "/signout"); using var server = host.GetTestServer(); var transaction = await SendAsync(server, "http://example.com/signout?X-Requested-With=XMLHttpRequest&ReturnUrl=/"); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); var responded = transaction.Response.Headers.GetValues("Location"); Assert.Single(responded); Assert.StartsWith("/", responded.Single()); } [Theory] [InlineData("/\tfoo")] [InlineData("/\rfoo")] [InlineData("/\nfoo")] [InlineData("/\r\nfoo")] [InlineData("/\0foo")] [InlineData("/\u0001foo")] [InlineData("/\u001Ffoo")] [InlineData("/\u007Ffoo")] [InlineData("/foo\r\nLocation:%20evil")] [InlineData("/foo\tbar")] public async Task LogoutReturnUrlWithControlCharacterIsRejected(string returnUrl) { using var host = await CreateHost(o => o.LogoutPath = "/signout"); using var server = host.GetTestServer(); var transaction = await SendAsync( server, "http://example.com/signout?ReturnUrl=" + Uri.EscapeDataString(returnUrl)); Assert.False(transaction.Response.Headers.Contains("Location")); } [Theory] [InlineData("/")] [InlineData("/foo")] [InlineData("/foo/bar")] [InlineData("/foo?x=1#y")] public async Task LogoutReturnUrlWithoutControlCharacterIsHonored(string returnUrl) { using var host = await CreateHost(o => o.LogoutPath = "/signout"); using var server = host.GetTestServer(); var transaction = await SendAsync( server, "http://example.com/signout?ReturnUrl=" + Uri.EscapeDataString(returnUrl)); var location = Assert.Single(transaction.Response.Headers.GetValues("Location")); Assert.Equal(returnUrl, location); } [Theory] // Protocol-relative URLs (path[1] == '/'). [InlineData("//www.example.com")] [InlineData("//www.example.com/foobar.html")] [InlineData("///www.example.com")] [InlineData("//////www.example.com")] // Forward-then-backslash (path[1] == '\\'). [InlineData("/\\")] [InlineData("/\\foo")] [InlineData("/\\evil.com")] // Application-relative (~) is rejected — cookie auth never resolves PathBase like MVC's IUrlHelper.Content does. [InlineData("~/")] [InlineData("~/foo")] [InlineData("~/foo.html")] // Absolute URLs (path[0] != '/'). [InlineData("http://www.example.com")] [InlineData("https://www.example.com")] [InlineData("HtTpS://www.example.com")] [InlineData("http://localhost/foobar.html")] [InlineData("javascript:alert(1)")] // Relative URLs (path[0] != '/'). [InlineData("foo.html")] [InlineData("../foo.html")] [InlineData("fold/foo.html")] // Single-slash scheme confusion. [InlineData("http:/foo.html")] [InlineData("hTtP:foo.html")] // Whitespace-only (length 1, path[0] != '/'). [InlineData(" ")] public async Task LogoutReturnUrlNonHostRelativeIsRejected(string returnUrl) { using var host = await CreateHost(o => o.LogoutPath = "/signout"); using var server = host.GetTestServer(); var transaction = await SendAsync( server, "http://example.com/signout?ReturnUrl=" + Uri.EscapeDataString(returnUrl)); Assert.False(transaction.Response.Headers.Contains("Location")); } [Fact] public async Task AjaxChallengeRedirectTurnsInto200WithLocationHeader() { using var host = await CreateHost(s => { }); using var server = host.GetTestServer(); var transaction = await SendAsync(server, "http://example.com/challenge?X-Requested-With=XMLHttpRequest&ReturnUrl=/"); Assert.Equal(HttpStatusCode.Unauthorized, transaction.Response.StatusCode); var responded = transaction.Response.Headers.GetValues("Location"); Assert.Single(responded); Assert.StartsWith("http://example.com/Account/Login", responded.Single()); } [Fact] public async Task CanConfigure401ChallengeInsteadOfRedirectWithMetadata() { using var host = await CreateHost(s => { }); using var server = host.GetTestServer(); var transaction = await SendAsync(server, "http://example.com/api/challenge"); Assert.Equal(HttpStatusCode.Unauthorized, transaction.Response.StatusCode); var responded = transaction.Response.Headers.GetValues("Location"); Assert.Single(responded); Assert.StartsWith("http://example.com/Account/Login", responded.Single()); } [Fact] public async Task CanConfigure403ForbiddenInsteadOfRedirectWithMetadata() { using var host = await CreateHost(s => { }); using var server = host.GetTestServer(); var transaction = await SendAsync(server, "http://example.com/api/forbid"); Assert.Equal(HttpStatusCode.Forbidden, transaction.Response.StatusCode); var responded = transaction.Response.Headers.GetValues("Location"); Assert.Single(responded); Assert.StartsWith("http://example.com/Account/AccessDenied", responded.Single()); } [Fact] public async Task CanReenableLoginRedirectWithMetadata() { using var host = await CreateHost(s => { }); using var server = host.GetTestServer(); var transaction = await SendAsync(server, "http://example.com/api/jk/challenge"); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); var responded = transaction.Response.Headers.GetValues("Location"); Assert.Single(responded); Assert.StartsWith("http://example.com/Account/Login", responded.Single()); } [Fact] public async Task CanReenableAccessDeniedRedirectWithMetadata() { using var host = await CreateHost(s => { }); using var server = host.GetTestServer(); var transaction = await SendAsync(server, "http://example.com/api/jk/forbid"); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); var responded = transaction.Response.Headers.GetValues("Location"); Assert.Single(responded); Assert.StartsWith("http://example.com/Account/AccessDenied", responded.Single()); } [Fact] public async Task ProtectedCustomRequestShouldRedirectToCustomRedirectUri() { using var host = await CreateHost(s => { }); using var server = host.GetTestServer(); var transaction = await SendAsync(server, "http://example.com/protected/CustomRedirect"); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); var location = transaction.Response.Headers.Location; Assert.Equal("http://example.com/Account/Login?ReturnUrl=%2FCustomRedirect", location.ToString()); } private static Task SignInAsAlice(HttpContext context) { var user = new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")); user.AddClaim(new Claim("marker", "true")); return context.SignInAsync("Cookies", new ClaimsPrincipal(user), new AuthenticationProperties()); } private static Task SignInAsWrong(HttpContext context) { return context.SignInAsync("Oops", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))), new AuthenticationProperties()); } private Task SignOutAsWrong(HttpContext context) { return context.SignOutAsync("Oops"); } [Fact] public async Task SignInCausesDefaultCookieToBeCreated() { using var host = await CreateHostWithServices(s => s.AddAuthentication().AddCookie(o => { o.LoginPath = new PathString("/login"); o.Cookie.Name = "TestCookie"; }), SignInAsAlice); using var server = host.GetTestServer(); var transaction = await SendAsync(server, "http://example.com/testpath"); var setCookie = transaction.SetCookie; Assert.StartsWith("TestCookie=", setCookie); Assert.Contains("; path=/", setCookie); Assert.Contains("; httponly", setCookie); Assert.Contains("; samesite=", setCookie); Assert.DoesNotContain("; expires=", setCookie); Assert.DoesNotContain("; domain=", setCookie); Assert.DoesNotContain("; secure", setCookie); Assert.True(transaction.Response.Headers.CacheControl.NoCache); Assert.True(transaction.Response.Headers.CacheControl.NoStore); Assert.Equal("no-cache", transaction.Response.Headers.Pragma.ToString()); } private class TestTicketStore : ITicketStore { private const string KeyPrefix = "AuthSessionStore-"; public readonly Dictionary<string, AuthenticationTicket> Store = new Dictionary<string, AuthenticationTicket>(); public async Task<string> StoreAsync(AuthenticationTicket ticket) { var guid = Guid.NewGuid(); var key = KeyPrefix + guid.ToString(); await RenewAsync(key, ticket); return key; } public Task RenewAsync(string key, AuthenticationTicket ticket) { Store[key] = ticket; return Task.FromResult(0); } public Task<AuthenticationTicket> RetrieveAsync(string key) { AuthenticationTicket ticket; Store.TryGetValue(key, out ticket); return Task.FromResult(ticket); } public Task RemoveAsync(string key) { Store.Remove(key); return Task.FromResult(0); } } [Fact] public async Task SignInWithTicketStoreWorks() { var sessionStore = new TestTicketStore(); using var host = await CreateHostWithServices(s => { s.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(o => { o.TimeProvider = _timeProvider; o.SessionStore = sessionStore; }); }, SignInAsAlice); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); // Make sure we have one key as the session id var key1 = Assert.Single(sessionStore.Store.Keys); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); // Make sure the session is expired _timeProvider.Advance(TimeSpan.FromDays(60)); // Verify that a new session is generated with a new key var transaction3 = await SendAsync(server, "http://example.com/signinalice", transaction1.CookieNameValue); var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue); var key2 = Assert.Single(sessionStore.Store.Keys); Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name)); Assert.NotEqual(key1, key2); } [Fact] public async Task SessionStoreRemovesExpired() { var sessionStore = new TestTicketStore(); using var host = await CreateHostWithServices(s => { s.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(o => { o.TimeProvider = _timeProvider; o.SessionStore = sessionStore; }); }, SignInAsAlice); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); // Make sure we have one key as the session id var key1 = Assert.Single(sessionStore.Store.Keys); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); // Make sure the session is expired _timeProvider.Advance(TimeSpan.FromDays(60)); // Verify that a new session is generated with a new key var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Empty(sessionStore.Store.Keys); Assert.Null(FindClaimValue(transaction3, ClaimTypes.Name)); } [Fact] public async Task SignOutClearsSessionKeySoSubsequentSignInGeneratesNewKey() { var sessionStore = new TestTicketStore(); using var host = await CreateHostWithServices(s => { s.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(o => { o.TimeProvider = _timeProvider; o.SessionStore = sessionStore; }); }, async context => { if (context.Request.Query.ContainsKey("signoutfirst")) { await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); // Sign back in as a different principal to mirror the privilege-rotation // scenario from https://github.com/dotnet/aspnetcore/issues/47503. var bob = new ClaimsIdentity(new GenericIdentity("Bob", "Cookies")); await context.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(bob), new AuthenticationProperties()); } else { await SignInAsAlice(context); } }); using var server = host.GetTestServer(); // Establish an initial session as Alice with one key in the store. var transaction1 = await SendAsync(server, "http://example.com/testpath"); var key1 = Assert.Single(sessionStore.Store.Keys); // In the same request, sign out and then sign back in as Bob while attaching the // existing cookie. SignOutAsync must clear the cached session key so that the // subsequent SignInAsync generates a fresh key via StoreAsync rather than reusing // the just-removed key via RenewAsync. // See https://github.com/dotnet/aspnetcore/issues/47503. // // Same-request SignOut + SignIn emits two Set-Cookie headers (the sign-out // delete-cookie followed by the sign-in auth-cookie), so the shared SendAsync // helper's SingleOrDefault on Set-Cookie can't be used here. Extract the // auth-cookie (the last Set-Cookie value) manually. var request2 = new HttpRequestMessage(HttpMethod.Get, "http://example.com/testpath?signoutfirst=1"); request2.Headers.Add("Cookie", transaction1.CookieNameValue); using var response2 = await server.CreateClient().SendAsync(request2); Assert.Equal(HttpStatusCode.OK, response2.StatusCode); var key2 = Assert.Single(sessionStore.Store.Keys); Assert.NotEqual(key1, key2); var setCookies = response2.Headers.GetValues("Set-Cookie").ToArray(); Assert.Equal(2, setCookies.Length); var authCookieNameValue = setCookies[^1].Split(';', 2)[0]; // The new cookie must actually authenticate, and it must resolve to Bob rather // than the pre-signout Alice identity. var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", authCookieNameValue); Assert.Equal("Bob", FindClaimValue(transaction3, ClaimTypes.Name)); } [Fact] public async Task CustomAuthSchemeEncodesCookieName() { var schemeName = "With spaces and 界"; using var host = await CreateHostWithServices(s => s.AddAuthentication(schemeName).AddCookie(schemeName, o => { o.LoginPath = new PathString("/login"); }), context => { var user = new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")); user.AddClaim(new Claim("marker", "true")); return context.SignInAsync(schemeName, new ClaimsPrincipal(user), new AuthenticationProperties()); }); using var server = host.GetTestServer(); var transaction = await SendAsync(server, "http://example.com/testpath"); var setCookie = transaction.SetCookie; Assert.StartsWith(".AspNetCore.With%20spaces%20and%20%E7%95%8C=", setCookie); Assert.Contains("; path=/", setCookie); Assert.Contains("; httponly", setCookie); Assert.Contains("; samesite=", setCookie); Assert.DoesNotContain("; expires=", setCookie); Assert.DoesNotContain("; domain=", setCookie); Assert.DoesNotContain("; secure", setCookie); Assert.True(transaction.Response.Headers.CacheControl.NoCache); Assert.True(transaction.Response.Headers.CacheControl.NoStore); Assert.Equal("no-cache", transaction.Response.Headers.Pragma.ToString()); } [Fact] public void SettingCookieExpirationOptionThrows() { var services = new ServiceCollection(); services.AddAuthentication().AddCookie(o => { o.Cookie.Expiration = TimeSpan.FromDays(10); }); var options = services.BuildServiceProvider().GetRequiredService<IOptionsMonitor<CookieAuthenticationOptions>>(); Assert.Throws<OptionsValidationException>(() => options.Get(CookieAuthenticationDefaults.AuthenticationScheme)); } [Fact] public async Task SignInWrongAuthTypeThrows() { using var host = await CreateHost(o => { o.LoginPath = new PathString("/login"); o.Cookie.Name = "TestCookie"; }, SignInAsWrong); using var server = host.GetTestServer(); await Assert.ThrowsAsync<InvalidOperationException>(async () => await SendAsync(server, "http://example.com/testpath")); } [Fact] public async Task SignOutWrongAuthTypeThrows() { using var host = await CreateHost(o => { o.LoginPath = new PathString("/login"); o.Cookie.Name = "TestCookie"; }, SignOutAsWrong); using var server = host.GetTestServer(); await Assert.ThrowsAsync<InvalidOperationException>(async () => await SendAsync(server, "http://example.com/testpath")); } [Theory] [InlineData(CookieSecurePolicy.Always, "http://example.com/testpath", true)] [InlineData(CookieSecurePolicy.Always, "https://example.com/testpath", true)] [InlineData(CookieSecurePolicy.None, "http://example.com/testpath", false)] [InlineData(CookieSecurePolicy.None, "https://example.com/testpath", false)] [InlineData(CookieSecurePolicy.SameAsRequest, "http://example.com/testpath", false)] [InlineData(CookieSecurePolicy.SameAsRequest, "https://example.com/testpath", true)] public async Task SecureSignInCausesSecureOnlyCookieByDefault( CookieSecurePolicy cookieSecurePolicy, string requestUri, bool shouldBeSecureOnly) { using var host = await CreateHost(o => { o.LoginPath = new PathString("/login"); o.Cookie.Name = "TestCookie"; o.Cookie.SecurePolicy = cookieSecurePolicy; }, SignInAsAlice); using var server = host.GetTestServer(); var transaction = await SendAsync(server, requestUri); var setCookie = transaction.SetCookie; if (shouldBeSecureOnly) { Assert.Contains("; secure", setCookie); } else { Assert.DoesNotContain("; secure", setCookie); } } [Fact] public async Task CookieOptionsAlterSetCookieHeader() { using var host = await CreateHost(o => { o.Cookie.Name = "TestCookie"; o.Cookie.Path = "/foo"; o.Cookie.Domain = "another.com"; o.Cookie.SecurePolicy = CookieSecurePolicy.Always; o.Cookie.SameSite = SameSiteMode.None; o.Cookie.HttpOnly = true; o.Cookie.Extensions.Add("extension0"); o.Cookie.Extensions.Add("extension1=value1"); }, SignInAsAlice, baseAddress: new Uri("http://example.com/base")); using var server1 = host.GetTestServer(); var transaction1 = await SendAsync(server1, "http://example.com/base/testpath"); var setCookie1 = transaction1.SetCookie; Assert.Contains("TestCookie=", setCookie1); Assert.Contains(" path=/foo", setCookie1); Assert.Contains(" domain=another.com", setCookie1); Assert.Contains(" secure", setCookie1); Assert.Contains(" samesite=none", setCookie1); Assert.Contains(" httponly", setCookie1); Assert.Contains(" extension0", setCookie1); Assert.Contains(" extension1=value1", setCookie1); using var host2 = await CreateHost(o => { o.Cookie.Name = "SecondCookie"; o.Cookie.SecurePolicy = CookieSecurePolicy.None; o.Cookie.SameSite = SameSiteMode.Strict; o.Cookie.HttpOnly = false; }, SignInAsAlice, baseAddress: new Uri("http://example.com/base")); using var server2 = host2.GetTestServer(); var transaction2 = await SendAsync(server2, "http://example.com/base/testpath"); var setCookie2 = transaction2.SetCookie; Assert.Contains("SecondCookie=", setCookie2); Assert.Contains(" path=/base", setCookie2); Assert.Contains(" samesite=strict", setCookie2); Assert.DoesNotContain(" domain=", setCookie2); Assert.DoesNotContain(" secure", setCookie2); Assert.DoesNotContain(" httponly", setCookie2); Assert.DoesNotContain(" extension", setCookie2); } [Fact] public async Task CookieContainsIdentity() { using var host = await CreateHost(o => { }, SignInAsAlice); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); } [Fact] public async Task CookieAppliesClaimsTransform() { using var host = await CreateHost(o => { }, SignInAsAlice, baseAddress: null, claimsTransform: true); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); Assert.Equal("yup", FindClaimValue(transaction2, "xform")); Assert.Null(FindClaimValue(transaction2, "sync")); } [Fact] public async Task CookieStopsWorkingAfterExpiration() { using var host = await CreateHost(o => { o.ExpireTimeSpan = TimeSpan.FromMinutes(10); o.SlidingExpiration = false; }, SignInAsAlice); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); _timeProvider.Advance(TimeSpan.FromMinutes(7)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); _timeProvider.Advance(TimeSpan.FromMinutes(7)); var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Null(transaction2.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); Assert.Null(transaction3.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); Assert.Null(transaction4.SetCookie); Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name)); } [Fact] public async Task CookieExpirationCanBeOverridenInSignin() { using var host = await CreateHost(o => { o.ExpireTimeSpan = TimeSpan.FromMinutes(10); o.SlidingExpiration = false; }, context => context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))), new AuthenticationProperties() { ExpiresUtc = _timeProvider.GetUtcNow().Add(TimeSpan.FromMinutes(5)) })); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); _timeProvider.Advance(TimeSpan.FromMinutes(3)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); _timeProvider.Advance(TimeSpan.FromMinutes(3)); var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Null(transaction2.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); Assert.Null(transaction3.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); Assert.Null(transaction4.SetCookie); Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name)); } [Fact] public async Task ExpiredCookieWithValidatorStillExpired() { using var host = await CreateHost(o => { o.ExpireTimeSpan = TimeSpan.FromMinutes(10); o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = ctx => { ctx.ShouldRenew = true; return Task.FromResult(0); } }; }, context => context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); _timeProvider.Advance(TimeSpan.FromMinutes(11)); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Null(transaction2.SetCookie); Assert.Null(FindClaimValue(transaction2, ClaimTypes.Name)); } [Fact] public async Task CookieCanBeRejectedAndSignedOutByValidator() { using var host = await CreateHost(o => { o.ExpireTimeSpan = TimeSpan.FromMinutes(10); o.SlidingExpiration = false; o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = ctx => { ctx.RejectPrincipal(); ctx.HttpContext.SignOutAsync("Cookies"); return Task.FromResult(0); } }; }, context => context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Contains(".AspNetCore.Cookies=; expires=", transaction2.SetCookie); Assert.Null(FindClaimValue(transaction2, ClaimTypes.Name)); } [Fact] public async Task CookieNotRenewedAfterSignOut() { using var host = await CreateHost(o => { o.ExpireTimeSpan = TimeSpan.FromMinutes(10); o.SlidingExpiration = false; o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = ctx => { ctx.ShouldRenew = true; return Task.FromResult(0); } }; }, context => context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); // renews on every request var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.NotNull(transaction2.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); var transaction3 = await server.SendAsync("http://example.com/normal", transaction1.CookieNameValue); Assert.NotNull(transaction3.SetCookie[0]); // signout wins over renew var transaction4 = await server.SendAsync("http://example.com/signout", transaction3.SetCookie[0]); Assert.Single(transaction4.SetCookie); Assert.Contains(".AspNetCore.Cookies=; expires=", transaction4.SetCookie[0]); } [Fact] public async Task CookieCanBeRenewedByValidator() { using var host = await CreateHost(o => { o.ExpireTimeSpan = TimeSpan.FromMinutes(10); o.SlidingExpiration = false; o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = ctx => { ctx.ShouldRenew = true; return Task.FromResult(0); } }; }, context => context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.NotNull(transaction2.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(5)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); Assert.NotNull(transaction3.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(6)); var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Null(transaction4.SetCookie); Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(5)); var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); Assert.Null(transaction5.SetCookie); Assert.Null(FindClaimValue(transaction5, ClaimTypes.Name)); } [Fact] public async Task CookieCanBeReplacedByValidator() { using var host = await CreateHost(o => { o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = ctx => { ctx.ShouldRenew = true; ctx.ReplacePrincipal(new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice2", "Cookies2")))); return Task.FromResult(0); } }; }, context => context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.NotNull(transaction2.SetCookie); Assert.Equal("Alice2", FindClaimValue(transaction2, ClaimTypes.Name)); } [Fact] public async Task CookieCanBeUpdatedByValidatorDuringRefresh() { var replace = false; using var host = await CreateHost(o => { o.ExpireTimeSpan = TimeSpan.FromMinutes(10); o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = ctx => { if (replace) { ctx.ShouldRenew = true; ctx.ReplacePrincipal(new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice2", "Cookies2")))); ctx.Properties.Items["updated"] = "yes"; } return Task.FromResult(0); } }; }, context => context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); Assert.Null(FindPropertiesValue(transaction3, "updated")); replace = true; var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.NotNull(transaction4.SetCookie); Assert.Equal("Alice2", FindClaimValue(transaction4, ClaimTypes.Name)); Assert.Equal("yes", FindPropertiesValue(transaction4, "updated")); replace = false; var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue); Assert.Equal("Alice2", FindClaimValue(transaction5, ClaimTypes.Name)); Assert.Equal("yes", FindPropertiesValue(transaction4, "updated")); } [Fact] public async Task CookieCanBeRenewedByValidatorWithSlidingExpiry() { using var host = await CreateHost(o => { o.ExpireTimeSpan = TimeSpan.FromMinutes(10); o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = ctx => { ctx.ShouldRenew = true; return Task.FromResult(0); } }; }, context => context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.NotNull(transaction2.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(5)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); Assert.NotNull(transaction3.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(6)); var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue); Assert.NotNull(transaction4.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(11)); var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue); Assert.Null(transaction5.SetCookie); Assert.Null(FindClaimValue(transaction5, ClaimTypes.Name)); } [Fact] public async Task CookieCanBeRenewedByValidatorWithModifiedProperties() { using var host = await CreateHost(o => { o.ExpireTimeSpan = TimeSpan.FromMinutes(10); o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = ctx => { ctx.ShouldRenew = true; var id = ctx.Principal.Identities.First(); var claim = id.FindFirst("counter"); if (claim == null) { id.AddClaim(new Claim("counter", "1")); } else { id.RemoveClaim(claim); id.AddClaim(new Claim("counter", claim.Value + "1")); } return Task.FromResult(0); } }; }, context => context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.NotNull(transaction2.SetCookie); Assert.Equal("1", FindClaimValue(transaction2, "counter")); _timeProvider.Advance(TimeSpan.FromMinutes(5)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); Assert.NotNull(transaction3.SetCookie); Assert.Equal("11", FindClaimValue(transaction3, "counter")); _timeProvider.Advance(TimeSpan.FromMinutes(6)); var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue); Assert.NotNull(transaction4.SetCookie); Assert.Equal("111", FindClaimValue(transaction4, "counter")); _timeProvider.Advance(TimeSpan.FromMinutes(11)); var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue); Assert.Null(transaction5.SetCookie); Assert.Null(FindClaimValue(transaction5, "counter")); } [Fact] public async Task CookieCanBeRenewedByValidatorWithModifiedLifetime() { using var host = await CreateHost(o => { o.ExpireTimeSpan = TimeSpan.FromMinutes(10); o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = ctx => { ctx.ShouldRenew = true; var id = ctx.Principal.Identities.First(); var claim = id.FindFirst("counter"); if (claim == null) { id.AddClaim(new Claim("counter", "1")); } else { id.RemoveClaim(claim); id.AddClaim(new Claim("counter", claim.Value + "1")); } // Causes the expiry time to not be extended because the lifetime is // calculated relative to the issue time. ctx.Properties.IssuedUtc = _timeProvider.GetUtcNow(); return Task.FromResult(0); } }; }, context => context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.NotNull(transaction2.SetCookie); Assert.Equal("1", FindClaimValue(transaction2, "counter")); _timeProvider.Advance(TimeSpan.FromMinutes(1)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); Assert.NotNull(transaction3.SetCookie); Assert.Equal("11", FindClaimValue(transaction3, "counter")); _timeProvider.Advance(TimeSpan.FromMinutes(1)); var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue); Assert.NotNull(transaction4.SetCookie); Assert.Equal("111", FindClaimValue(transaction4, "counter")); _timeProvider.Advance(TimeSpan.FromMinutes(9)); var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue); Assert.Null(transaction5.SetCookie); Assert.Null(FindClaimValue(transaction5, "counter")); } [Fact] public async Task CookieValidatorOnlyCalledOnce() { using var host = await CreateHost(o => { o.ExpireTimeSpan = TimeSpan.FromMinutes(10); o.SlidingExpiration = false; o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = ctx => { ctx.ShouldRenew = true; return Task.FromResult(0); } }; }, context => context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.NotNull(transaction2.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(5)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); Assert.NotNull(transaction3.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(6)); var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Null(transaction4.SetCookie); Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(5)); var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); Assert.Null(transaction5.SetCookie); Assert.Null(FindClaimValue(transaction5, ClaimTypes.Name)); } [Theory] [InlineData(true)] [InlineData(false)] public async Task ShouldRenewUpdatesIssuedExpiredUtc(bool sliding) { DateTimeOffset? lastValidateIssuedDate = null; DateTimeOffset? lastExpiresDate = null; using var host = await CreateHost(o => { o.ExpireTimeSpan = TimeSpan.FromMinutes(10); o.SlidingExpiration = sliding; o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = ctx => { lastValidateIssuedDate = ctx.Properties.IssuedUtc; lastExpiresDate = ctx.Properties.ExpiresUtc; ctx.ShouldRenew = true; return Task.FromResult(0); } }; }, context => context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))))); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.NotNull(transaction2.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); Assert.NotNull(lastValidateIssuedDate); Assert.NotNull(lastExpiresDate); var firstIssueDate = lastValidateIssuedDate; var firstExpiresDate = lastExpiresDate; _timeProvider.Advance(TimeSpan.FromMinutes(1)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue); Assert.NotNull(transaction3.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(2)); var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue); Assert.NotNull(transaction4.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name)); Assert.NotEqual(lastValidateIssuedDate, firstIssueDate); Assert.NotEqual(firstExpiresDate, lastExpiresDate); } [Fact] public async Task CookieExpirationCanBeOverridenInEvent() { using var host = await CreateHost(o => { o.ExpireTimeSpan = TimeSpan.FromMinutes(10); o.SlidingExpiration = false; o.Events = new CookieAuthenticationEvents() { OnSigningIn = context => { context.Properties.ExpiresUtc = _timeProvider.GetUtcNow().Add(TimeSpan.FromMinutes(5)); return Task.FromResult(0); } }; }, SignInAsAlice); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Null(transaction2.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(3)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Null(transaction3.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(3)); var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Null(transaction4.SetCookie); Assert.Null(FindClaimValue(transaction4, ClaimTypes.Name)); } [Fact] public async Task CookieIsRenewedWithSlidingExpiration() { using var host = await CreateHost(o => { o.ExpireTimeSpan = TimeSpan.FromMinutes(10); o.SlidingExpiration = true; }, SignInAsAlice); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Null(transaction2.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(4)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Null(transaction3.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(4)); // transaction4 should arrive with a new SetCookie value var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.NotNull(transaction4.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(4)); var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue); Assert.Null(transaction5.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction5, ClaimTypes.Name)); } [Fact] public async Task CookieIsRenewedWithSlidingExpirationWithoutTransformations() { using var host = await CreateHost(o => { o.ExpireTimeSpan = TimeSpan.FromMinutes(10); o.SlidingExpiration = true; o.Events.OnValidatePrincipal = c => { // https://github.com/aspnet/Security/issues/1607 // On sliding refresh the transformed principal should not be serialized into the cookie, only the original principal. Assert.Single(c.Principal.Identities); Assert.True(c.Principal.Identities.First().HasClaim("marker", "true")); return Task.CompletedTask; }; }, SignInAsAlice, claimsTransform: true); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Null(transaction2.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(4)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.Null(transaction3.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(4)); // transaction4 should arrive with a new SetCookie value var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); Assert.NotNull(transaction4.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(4)); var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue); Assert.Null(transaction5.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction5, ClaimTypes.Name)); } [Fact] public async Task CookieIsRenewedWithSlidingExpirationEvent() { using var host = await CreateHost(o => { o.ExpireTimeSpan = TimeSpan.FromMinutes(10); o.SlidingExpiration = true; o.Events = new CookieAuthenticationEvents() { OnCheckSlidingExpiration = context => { var expectRenew = string.Equals("1", context.Request.Query["expectrenew"]); var renew = string.Equals("1", context.Request.Query["renew"]); Assert.Equal(expectRenew, context.ShouldRenew); context.ShouldRenew = renew; return Task.CompletedTask; } }; }, SignInAsAlice); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies?expectrenew=0&renew=0", transaction1.CookieNameValue); Assert.Null(transaction2.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(4)); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies?expectrenew=0&renew=0", transaction1.CookieNameValue); Assert.Null(transaction3.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction3, ClaimTypes.Name)); _timeProvider.Advance(TimeSpan.FromMinutes(4)); // A renewal is now expected, but we've suppressed it var transaction4 = await SendAsync(server, "http://example.com/me/Cookies?expectrenew=1&renew=0", transaction1.CookieNameValue); Assert.Null(transaction4.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction4, ClaimTypes.Name)); // Allow the default renewal to happen var transaction5 = await SendAsync(server, "http://example.com/me/Cookies?expectrenew=1&renew=1", transaction1.CookieNameValue); Assert.NotNull(transaction5.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction5, ClaimTypes.Name)); // Force a renewal on an un-expired new cookie var transaction6 = await SendAsync(server, "http://example.com/me/Cookies?expectrenew=0&renew=1", transaction5.CookieNameValue); Assert.NotNull(transaction5.SetCookie); Assert.Equal("Alice", FindClaimValue(transaction6, ClaimTypes.Name)); } [Fact] public async Task CookieUsesPathBaseByDefault() { using var host = await CreateHost(o => { }, context => { Assert.Equal(new PathString("/base"), context.Request.PathBase); return context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))); }, new Uri("http://example.com/base")); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/base/testpath"); Assert.Contains("path=/base", transaction1.SetCookie); } [Fact] public async Task CookieChallengeRedirectsToLoginWithoutCookie() { using var host = await CreateHost(o => { }, SignInAsAlice); var url = "http://example.com/challenge"; using var server = host.GetTestServer(); var transaction = await SendAsync(server, url); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); var location = transaction.Response.Headers.Location; Assert.Equal("/Account/Login", location.LocalPath); } [Fact] public async Task CookieForbidRedirectsWithoutCookie() { using var host = await CreateHost(o => { }, SignInAsAlice); var url = "http://example.com/forbid"; using var server = host.GetTestServer(); var transaction = await SendAsync(server, url); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); var location = transaction.Response.Headers.Location; Assert.Equal("/Account/AccessDenied", location.LocalPath); } [Fact] public async Task CookieChallengeRedirectsWithLoginPath() { using var host = await CreateHost(o => { o.LoginPath = new PathString("/page"); }); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/challenge", transaction1.CookieNameValue); Assert.Equal(HttpStatusCode.Redirect, transaction2.Response.StatusCode); } [Fact] public async Task CookieChallengeWithUnauthorizedRedirectsToLoginIfNotAuthenticated() { using var host = await CreateHost(o => { o.LoginPath = new PathString("/page"); }); using var server = host.GetTestServer(); var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction2 = await SendAsync(server, "http://example.com/unauthorized", transaction1.CookieNameValue); Assert.Equal(HttpStatusCode.Redirect, transaction2.Response.StatusCode); } [Theory] [InlineData(true)] [InlineData(false)] public async Task MapWillAffectChallengeOnlyWithUseAuth(bool useAuth) { using var host = new HostBuilder() .ConfigureWebHost(builder => builder.UseTestServer() .Configure(app => { if (useAuth) { app.UseAuthentication(); } app.Map("/login", signoutApp => signoutApp.Run(context => context.ChallengeAsync("Cookies", new AuthenticationProperties() { RedirectUri = "/" }))); }) .ConfigureServices(s => s.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/page")))) .Build(); await host.StartAsync(); using var server = host.GetTestServer(); var transaction = await server.SendAsync("http://example.com/login"); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); var location = transaction.Response.Headers.Location; if (useAuth) { Assert.Equal("/page", location.LocalPath); } else { Assert.Equal("/login/page", location.LocalPath); } Assert.Equal("?ReturnUrl=%2F", location.Query); } [ConditionalFact(Skip = "Revisit, exception no longer thrown")] public async Task ChallengeDoesNotSet401OnUnauthorized() { using var host = new HostBuilder() .ConfigureWebHost(builder => builder.UseTestServer() .Configure(app => { app.UseAuthentication(); app.Run(async context => { await Assert.ThrowsAsync<InvalidOperationException>(() => context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme)); }); }) .ConfigureServices(services => services.AddAuthentication().AddCookie())) .Build(); await host.StartAsync(); using var server = host.GetTestServer(); var transaction = await server.SendAsync("http://example.com"); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); } [Fact] public async Task CanConfigureDefaultCookieInstance() { using var host = new HostBuilder() .ConfigureWebHost(builder => builder.UseTestServer() .Configure(app => { app.UseAuthentication(); app.Run(context => context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity("whatever")))); }) .ConfigureServices(services => { services.AddAuthentication().AddCookie(); services.Configure<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme, o => o.Cookie.Name = "One"); })) .Build(); await host.StartAsync(); using var server = host.GetTestServer(); var transaction = await server.SendAsync("http://example.com"); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); Assert.StartsWith("One=", transaction.SetCookie[0]); } [Fact] public async Task CanConfigureNamedCookieInstance() { using var host = new HostBuilder() .ConfigureWebHost(builder => builder.UseTestServer() .Configure(app => { app.UseAuthentication(); app.Run(context => context.SignInAsync("Cookie1", new ClaimsPrincipal(new ClaimsIdentity("whatever")))); }) .ConfigureServices(services => { services.AddAuthentication().AddCookie("Cookie1"); services.Configure<CookieAuthenticationOptions>("Cookie1", o => o.Cookie.Name = "One"); })) .Build(); await host.StartAsync(); using var server = host.GetTestServer(); var transaction = await server.SendAsync("http://example.com"); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); Assert.StartsWith("One=", transaction.SetCookie[0]); } [Fact] public async Task MapWithSignInOnlyRedirectToReturnUrlOnLoginPath() { using var host = new HostBuilder() .ConfigureWebHost(builder => builder.UseTestServer() .Configure(app => { app.UseAuthentication(); app.Map("/notlogin", signoutApp => signoutApp.Run(context => context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity("whatever"))))); }) .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/login")))) .Build(); await host.StartAsync(); using var server = host.GetTestServer(); var transaction = await server.SendAsync("http://example.com/notlogin?ReturnUrl=%2Fpage"); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); Assert.NotNull(transaction.SetCookie); } [Fact] public async Task MapWillNotAffectSignInRedirectToReturnUrl() { using var host = new HostBuilder() .ConfigureWebHost(builder => builder.UseTestServer() .Configure(app => { app.UseAuthentication(); app.Map("/login", signoutApp => signoutApp.Run(context => context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity("whatever"))))); }) .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/login")))) .Build(); await host.StartAsync(); using var server = host.GetTestServer(); var transaction = await server.SendAsync("http://example.com/login?ReturnUrl=%2Fpage"); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); Assert.NotNull(transaction.SetCookie); var location = transaction.Response.Headers.Location; Assert.Equal("/page", location.OriginalString); } [Fact] public async Task MapWithSignOutOnlyRedirectToReturnUrlOnLogoutPath() { using var host = new HostBuilder() .ConfigureWebHost(builder => builder.UseTestServer() .Configure(app => { app.UseAuthentication(); app.Map("/notlogout", signoutApp => signoutApp.Run(context => context.SignOutAsync("Cookies"))); }) .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LogoutPath = new PathString("/logout")))) .Build(); await host.StartAsync(); using var server = host.GetTestServer(); var transaction = await server.SendAsync("http://example.com/notlogout?ReturnUrl=%2Fpage"); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); Assert.Contains(".AspNetCore.Cookies=; expires=", transaction.SetCookie[0]); } [Fact] public async Task MapWillNotAffectSignOutRedirectToReturnUrl() { using var host = new HostBuilder() .ConfigureWebHost(builder => builder.UseTestServer() .Configure(app => { app.UseAuthentication(); app.Map("/logout", signoutApp => signoutApp.Run(context => context.SignOutAsync("Cookies"))); }) .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LogoutPath = new PathString("/logout")))) .Build(); await host.StartAsync(); using var server = host.GetTestServer(); var transaction = await server.SendAsync("http://example.com/logout?ReturnUrl=%2Fpage"); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); Assert.Contains(".AspNetCore.Cookies=; expires=", transaction.SetCookie[0]); var location = transaction.Response.Headers.Location; Assert.Equal("/page", location.OriginalString); } [Fact] public async Task MapWillNotAffectAccessDenied() { using var host = new HostBuilder() .ConfigureWebHost(builder => builder.UseTestServer() .Configure(app => { app.UseAuthentication(); app.Map("/forbid", signoutApp => signoutApp.Run(context => context.ForbidAsync("Cookies"))); }) .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.AccessDeniedPath = new PathString("/denied")))) .Build(); await host.StartAsync(); using var server = host.GetTestServer(); var transaction = await server.SendAsync("http://example.com/forbid"); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); var location = transaction.Response.Headers.Location; Assert.Equal("/denied", location.LocalPath); } [Fact] public async Task NestedMapWillNotAffectLogin() { using var host = new HostBuilder() .ConfigureWebHost(builder => builder.UseTestServer() .Configure(app => app.Map("/base", map => { map.UseAuthentication(); map.Map("/login", signoutApp => signoutApp.Run(context => context.ChallengeAsync("Cookies", new AuthenticationProperties() { RedirectUri = "/" }))); })) .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.LoginPath = new PathString("/page")))) .Build(); await host.StartAsync(); using var server = host.GetTestServer(); var transaction = await server.SendAsync("http://example.com/base/login"); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); var location = transaction.Response.Headers.Location; Assert.Equal("/base/page", location.LocalPath); Assert.Equal("?ReturnUrl=%2F", location.Query); } [Theory] [InlineData("/redirect_test", "/loginpath")] [InlineData("/redirect_test", "/testpath")] [InlineData("http://example.com/redirect_to", "/loginpath")] [InlineData("http://example.com/redirect_to", "/testpath")] public async Task RedirectUriIsHonoredAfterSignin(string redirectUrl, string loginPath) { using var host = await CreateHost(o => { o.LoginPath = loginPath; o.Cookie.Name = "TestCookie"; }, async context => await context.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme))), new AuthenticationProperties { RedirectUri = redirectUrl }) ); using var server = host.GetTestServer(); var transaction = await SendAsync(server, "http://example.com/testpath"); Assert.NotEmpty(transaction.SetCookie); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); Assert.Equal(redirectUrl, transaction.Response.Headers.Location.ToString()); } [Fact] public async Task RedirectUriInQueryIsIgnoredAfterSigninForUnrecognizedEndpoints() { using var host = await CreateHost(o => { o.LoginPath = "/loginpath"; o.ReturnUrlParameter = "return"; o.Cookie.Name = "TestCookie"; }, async context => { await context.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme)))); }); using var server = host.GetTestServer(); var transaction = await SendAsync(server, "http://example.com/testpath?return=%2Fret_path_2"); Assert.NotEmpty(transaction.SetCookie); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); } [Fact] public async Task RedirectUriInQueryIsHonoredAfterSignin() { using var host = await CreateHost(o => { o.LoginPath = "/testpath"; o.ReturnUrlParameter = "return"; o.Cookie.Name = "TestCookie"; }, async context => { await context.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme)))); }); using var server = host.GetTestServer(); var transaction = await SendAsync(server, "http://example.com/testpath?return=%2Fret_path_2"); Assert.NotEmpty(transaction.SetCookie); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); Assert.Equal("/ret_path_2", transaction.Response.Headers.Location.ToString()); } [Fact] public async Task AbsoluteRedirectUriInQueryStringIsRejected() { using var host = await CreateHost(o => { o.LoginPath = "/testpath"; o.ReturnUrlParameter = "return"; o.Cookie.Name = "TestCookie"; }, async context => { await context.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme)))); }); using var server = host.GetTestServer(); var transaction = await SendAsync(server, "http://example.com/testpath?return=http%3A%2F%2Fexample.com%2Fredirect_to"); Assert.NotEmpty(transaction.SetCookie); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); } [Fact] public async Task EnsurePrecedenceOfRedirectUriAfterSignin() { using var host = await CreateHost(o => { o.LoginPath = "/testpath"; o.ReturnUrlParameter = "return"; o.Cookie.Name = "TestCookie"; }, async context => { await context.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", CookieAuthenticationDefaults.AuthenticationScheme))), new AuthenticationProperties { RedirectUri = "/redirect_test" }); }); using var server = host.GetTestServer(); var transaction = await SendAsync(server, "http://example.com/testpath?return=%2Fret_path_2"); Assert.NotEmpty(transaction.SetCookie); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); Assert.Equal("/redirect_test", transaction.Response.Headers.Location.ToString()); } [Fact] public async Task NestedMapWillNotAffectAccessDenied() { using var host = new HostBuilder() .ConfigureWebHost(builder => builder.UseTestServer() .Configure(app => app.Map("/base", map => { map.UseAuthentication(); map.Map("/forbid", signoutApp => signoutApp.Run(context => context.ForbidAsync("Cookies"))); })) .ConfigureServices(services => services.AddAuthentication().AddCookie(o => o.AccessDeniedPath = new PathString("/denied")))) .Build(); await host.StartAsync(); using var server = host.GetTestServer(); var transaction = await server.SendAsync("http://example.com/base/forbid"); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); var location = transaction.Response.Headers.Location; Assert.Equal("/base/denied", location.LocalPath); } [Fact] public async Task CanSpecifyAndShareDataProtector() { var dp = new NoOpDataProtector(); using var host1 = new HostBuilder() .ConfigureWebHost(builder => builder.UseTestServer() .Configure(app => { app.UseAuthentication(); app.Run((context) => context.SignInAsync("Cookies", new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies"))), new AuthenticationProperties())); }) .ConfigureServices(services => services.AddAuthentication().AddCookie(o => { o.TicketDataFormat = new TicketDataFormat(dp); o.Cookie.Name = "Cookie"; }))) .Build(); await host1.StartAsync(); using var server1 = host1.GetTestServer(); var transaction = await SendAsync(server1, "http://example.com/stuff"); Assert.NotNull(transaction.SetCookie); using var host2 = new HostBuilder() .ConfigureWebHost(builder => builder.UseTestServer() .Configure(app => { app.UseAuthentication(); app.Run(async (context) => { var result = await context.AuthenticateAsync("Cookies"); await DescribeAsync(context.Response, result); }); }) .ConfigureServices(services => services.AddAuthentication().AddCookie("Cookies", o => { o.Cookie.Name = "Cookie"; o.TicketDataFormat = new TicketDataFormat(dp); }))) .Build(); await host2.StartAsync(); using var server2 = host2.GetTestServer(); var transaction2 = await SendAsync(server2, "http://example.com/stuff", transaction.CookieNameValue); Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); } // Issue: https://github.com/aspnet/Security/issues/949 [Fact] public async Task NullExpiresUtcPropertyIsGuarded() { using var host = new HostBuilder() .ConfigureWebHost(builder => builder.UseTestServer() .ConfigureServices(services => services.AddAuthentication().AddCookie(o => { o.Events = new CookieAuthenticationEvents { OnValidatePrincipal = context => { context.Properties.ExpiresUtc = null; context.ShouldRenew = true; return Task.FromResult(0); } }; })) .Configure(app => { app.UseAuthentication(); app.Run(async context => { if (context.Request.Path == "/signin") { await context.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))); } else { await context.Response.WriteAsync("ha+1"); } }); })) .Build(); await host.StartAsync(); using var server = host.GetTestServer(); var cookie = (await server.SendAsync("http://www.example.com/signin")).SetCookie.FirstOrDefault(); Assert.NotNull(cookie); var transaction = await server.SendAsync("http://www.example.com/", cookie); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); } private class NoOpDataProtector : IDataProtector { public IDataProtector CreateProtector(string purpose) { return this; } public byte[] Protect(byte[] plaintext) { return plaintext; } public byte[] Unprotect(byte[] protectedData) { return protectedData; } } private static string FindClaimValue(Transaction transaction, string claimType) { var claim = transaction.ResponseElement.Elements("claim").SingleOrDefault(elt => elt.Attribute("type").Value == claimType); if (claim == null) { return null; } return claim.Attribute("value").Value; } private static string FindPropertiesValue(Transaction transaction, string key) { var property = transaction.ResponseElement.Elements("extra").SingleOrDefault(elt => elt.Attribute("type").Value == key); if (property == null) { return null; } return property.Attribute("value").Value; } private class ClaimsTransformer : IClaimsTransformation { public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal p) { var firstId = p.Identities.First(); if (firstId.HasClaim("marker", "true")) { firstId.RemoveClaim(firstId.FindFirst("marker")); } // TransformAsync could be called twice on one request if you have a default scheme and also // call AuthenticateAsync. if (!p.Identities.Any(i => i.AuthenticationType == "xform")) { var id = new ClaimsIdentity("xform"); id.AddClaim(new Claim("xform", "yup")); p.AddIdentity(id); } return Task.FromResult(p); } } private Task<IHost> CreateHost(Action<CookieAuthenticationOptions> configureOptions, Func<HttpContext, Task> testpath = null, Uri baseAddress = null, bool claimsTransform = false) => CreateHostWithServices(s => { s.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(o => { o.TimeProvider = _timeProvider; configureOptions(o); }); if (claimsTransform) { s.AddSingleton<IClaimsTransformation, ClaimsTransformer>(); } }, testpath, baseAddress); private static async Task<IHost> CreateHostWithServices(Action<IServiceCollection> configureServices, Func<HttpContext, Task> testpath = null, Uri baseAddress = null) { var host = new HostBuilder() .ConfigureWebHost(builder => builder.UseTestServer() .Configure(app => { app.UseAuthentication(); app.Use(async (context, next) => { var req = context.Request; var res = context.Response; PathString remainder; if (req.Path == new PathString("/normal")) { res.StatusCode = 200; } else if (req.Path == new PathString("/forbid")) // Simulate forbidden { await context.ForbidAsync(CookieAuthenticationDefaults.AuthenticationScheme); } else if (req.Path == new PathString("/challenge")) { await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme); } else if (req.Path == new PathString("/signinalice")) { await SignInAsAlice(context); } else if (req.Path == new PathString("/signout")) { await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); } else if (req.Path == new PathString("/unauthorized")) { await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties()); } else if (req.Path == new PathString("/protected/CustomRedirect")) { await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties() { RedirectUri = "/CustomRedirect" }); } else if (req.Path == new PathString("/me")) { await DescribeAsync(res, AuthenticateResult.Success(new AuthenticationTicket(context.User, new AuthenticationProperties(), CookieAuthenticationDefaults.AuthenticationScheme))); } else if (req.Path.StartsWithSegments(new PathString("/me"), out remainder)) { var ticket = await context.AuthenticateAsync(remainder.Value.Substring(1)); await DescribeAsync(res, ticket); } else if (req.Path == new PathString("/testpath") && testpath != null) { await testpath(context); } else if (req.Path == new PathString("/checkforerrors")) { var result = await context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); // this used to be "Automatic" if (result.Failure != null) { throw new Exception("Failed to authenticate", result.Failure); } return; } else { await next(context); } }); app.UseRouting(); app.UseEndpoints(endpoints => { void AddChallengeAndForbidEndpoints(IEndpointRouteBuilder routeGroup) { routeGroup.MapGet("/challenge", async context => { await context.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme); }); routeGroup.MapGet("/forbid", async context => { await context.ForbidAsync(CookieAuthenticationDefaults.AuthenticationScheme); }); } var apiRouteGroup = endpoints.MapGroup("/api").DisableCookieRedirect(); AddChallengeAndForbidEndpoints(apiRouteGroup); // IAllowCookieRedirect always wins if present. Adding IDisableCookieRedirect before and afterwards does not override it. var overriddenRouteGroup = apiRouteGroup.MapGroup("/jk").AllowCookieRedirect().DisableCookieRedirect(); AddChallengeAndForbidEndpoints(overriddenRouteGroup); }); }) .ConfigureServices(services => { services.AddRoutingCore(); configureServices(services); })) .Build(); await host.StartAsync(); var server = host.GetTestServer(); server.BaseAddress = baseAddress; return host; } private static Task DescribeAsync(HttpResponse res, AuthenticateResult result) { res.StatusCode = 200; res.ContentType = "text/xml"; var xml = new XElement("xml"); if (result?.Ticket?.Principal != null) { xml.Add(result.Ticket.Principal.Claims.Select(claim => new XElement("claim", new XAttribute("type", claim.Type), new XAttribute("value", claim.Value)))); } if (result?.Ticket?.Properties != null) { xml.Add(result.Ticket.Properties.Items.Select(extra => new XElement("extra", new XAttribute("type", extra.Key), new XAttribute("value", extra.Value)))); } var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString()); return res.Body.WriteAsync(xmlBytes, 0, xmlBytes.Length); } private static async Task<Transaction> SendAsync(TestServer server, string uri, string cookieHeader = null) { var request = new HttpRequestMessage(HttpMethod.Get, uri); if (!string.IsNullOrEmpty(cookieHeader)) { request.Headers.Add("Cookie", cookieHeader); } var transaction = new Transaction { Request = request, Response = await server.CreateClient().SendAsync(request), }; if (transaction.Response.Headers.Contains("Set-Cookie")) { transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").SingleOrDefault(); } if (!string.IsNullOrEmpty(transaction.SetCookie)) { transaction.CookieNameValue = transaction.SetCookie.Split(new[] { ';' }, 2).First(); } transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); if (transaction.Response.Content != null && transaction.Response.Content.Headers.ContentType != null && transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") { transaction.ResponseElement = XElement.Parse(transaction.ResponseText); } return transaction; } private class Transaction { public HttpRequestMessage Request { get; set; } public HttpResponseMessage Response { get; set; } public string SetCookie { get; set; } public string CookieNameValue { get; set; } public string ResponseText { get; set; } public XElement ResponseElement { get; set; } } }