aspnetcore/test/Microsoft.AspNet.Security.Test/Cookies/CookieMiddlewareTests.cs

482 lines
19 KiB
C#

// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Security;
using Microsoft.AspNet.Security.DataProtection;
using Microsoft.AspNet.TestHost;
using Shouldly;
using Xunit;
namespace Microsoft.AspNet.Security.Cookies
{
public class CookieMiddlewareTests
{
[Fact]
public async Task NormalRequestPassesThrough()
{
TestServer server = CreateServer(options =>
{
});
HttpResponseMessage response = await server.CreateClient().GetAsync("http://example.com/normal");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
}
[Fact]
public async Task ProtectedRequestShouldRedirectToLogin()
{
TestServer server = CreateServer(options =>
{
options.LoginPath = new PathString("/login");
});
Transaction transaction = await SendAsync(server, "http://example.com/protected");
transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect);
Uri location = transaction.Response.Headers.Location;
location.LocalPath.ShouldBe("/login");
location.Query.ShouldBe("?ReturnUrl=%2Fprotected");
}
[Fact]
public async Task ProtectedCustomRequestShouldRedirectToCustomLogin()
{
TestServer server = CreateServer(options =>
{
options.LoginPath = new PathString("/login");
});
Transaction transaction = await SendAsync(server, "http://example.com/protected/CustomRedirect");
transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect);
Uri location = transaction.Response.Headers.Location;
location.ToString().ShouldBe("/CustomRedirect");
}
private Task SignInAsAlice(HttpContext context)
{
context.Response.SignIn(
new AuthenticationProperties(),
new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")));
return Task.FromResult<object>(null);
}
[Fact]
public async Task SignInCausesDefaultCookieToBeCreated()
{
TestServer server = CreateServer(options =>
{
options.LoginPath = new PathString("/login");
options.CookieName = "TestCookie";
}, SignInAsAlice);
Transaction transaction = await SendAsync(server, "http://example.com/testpath");
string setCookie = transaction.SetCookie;
setCookie.ShouldStartWith("TestCookie=");
setCookie.ShouldContain("; path=/");
setCookie.ShouldContain("; HttpOnly");
setCookie.ShouldNotContain("; expires=");
setCookie.ShouldNotContain("; domain=");
setCookie.ShouldNotContain("; secure");
}
[Theory]
[InlineData(CookieSecureOption.Always, "http://example.com/testpath", true)]
[InlineData(CookieSecureOption.Always, "https://example.com/testpath", true)]
[InlineData(CookieSecureOption.Never, "http://example.com/testpath", false)]
[InlineData(CookieSecureOption.Never, "https://example.com/testpath", false)]
[InlineData(CookieSecureOption.SameAsRequest, "http://example.com/testpath", false)]
[InlineData(CookieSecureOption.SameAsRequest, "https://example.com/testpath", true)]
public async Task SecureSignInCausesSecureOnlyCookieByDefault(
CookieSecureOption cookieSecureOption,
string requestUri,
bool shouldBeSecureOnly)
{
TestServer server = CreateServer(options =>
{
options.LoginPath = new PathString("/login");
options.CookieName = "TestCookie";
options.CookieSecure = cookieSecureOption;
}, SignInAsAlice);
Transaction transaction = await SendAsync(server, requestUri);
string setCookie = transaction.SetCookie;
if (shouldBeSecureOnly)
{
setCookie.ShouldContain("; secure");
}
else
{
setCookie.ShouldNotContain("; secure");
}
}
[Fact]
public async Task CookieOptionsAlterSetCookieHeader()
{
TestServer server1 = CreateServer(options =>
{
options.CookieName = "TestCookie";
options.CookiePath = "/foo";
options.CookieDomain = "another.com";
options.CookieSecure = CookieSecureOption.Always;
options.CookieHttpOnly = true;
}, SignInAsAlice);
Transaction transaction1 = await SendAsync(server1, "http://example.com/testpath");
TestServer server2 = CreateServer(options =>
{
options.CookieName = "SecondCookie";
options.CookieSecure = CookieSecureOption.Never;
options.CookieHttpOnly = false;
}, SignInAsAlice);
Transaction transaction2 = await SendAsync(server2, "http://example.com/testpath");
string setCookie1 = transaction1.SetCookie;
string setCookie2 = transaction2.SetCookie;
setCookie1.ShouldContain("TestCookie=");
setCookie1.ShouldContain(" path=/foo");
setCookie1.ShouldContain(" domain=another.com");
setCookie1.ShouldContain(" secure");
setCookie1.ShouldContain(" HttpOnly");
setCookie2.ShouldContain("SecondCookie=");
setCookie2.ShouldNotContain(" domain=");
setCookie2.ShouldNotContain(" secure");
setCookie2.ShouldNotContain(" HttpOnly");
}
[Fact]
public async Task CookieContainsIdentity()
{
var clock = new TestClock();
TestServer server = CreateServer(options =>
{
options.SystemClock = clock;
}, SignInAsAlice);
Transaction transaction1 = await SendAsync(server, "http://example.com/testpath");
Transaction transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice");
}
[Fact]
public async Task CookieStopsWorkingAfterExpiration()
{
var clock = new TestClock();
TestServer server = CreateServer(options =>
{
options.SystemClock = clock;
options.ExpireTimeSpan = TimeSpan.FromMinutes(10);
options.SlidingExpiration = false;
}, SignInAsAlice);
Transaction transaction1 = await SendAsync(server, "http://example.com/testpath");
Transaction transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
clock.Add(TimeSpan.FromMinutes(7));
Transaction transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
clock.Add(TimeSpan.FromMinutes(7));
Transaction transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
transaction2.SetCookie.ShouldBe(null);
FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice");
transaction3.SetCookie.ShouldBe(null);
FindClaimValue(transaction3, ClaimTypes.Name).ShouldBe("Alice");
transaction4.SetCookie.ShouldBe(null);
FindClaimValue(transaction4, ClaimTypes.Name).ShouldBe(null);
}
[Fact]
public async Task CookieExpirationCanBeOverridenInSignin()
{
var clock = new TestClock();
TestServer server = CreateServer(options =>
{
options.SystemClock = clock;
options.ExpireTimeSpan = TimeSpan.FromMinutes(10);
options.SlidingExpiration = false;
},
context =>
{
context.Response.SignIn(
new AuthenticationProperties() { ExpiresUtc = clock.UtcNow.Add(TimeSpan.FromMinutes(5)) },
new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")));
return Task.FromResult<object>(null);
});
Transaction transaction1 = await SendAsync(server, "http://example.com/testpath");
Transaction transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
clock.Add(TimeSpan.FromMinutes(3));
Transaction transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
clock.Add(TimeSpan.FromMinutes(3));
Transaction transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
transaction2.SetCookie.ShouldBe(null);
FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice");
transaction3.SetCookie.ShouldBe(null);
FindClaimValue(transaction3, ClaimTypes.Name).ShouldBe("Alice");
transaction4.SetCookie.ShouldBe(null);
FindClaimValue(transaction4, ClaimTypes.Name).ShouldBe(null);
}
[Fact]
public async Task CookieExpirationCanBeOverridenInEvent()
{
var clock = new TestClock();
TestServer server = CreateServer(options =>
{
options.SystemClock = clock;
options.ExpireTimeSpan = TimeSpan.FromMinutes(10);
options.SlidingExpiration = false;
options.Notifications = new CookieAuthenticationNotifications()
{
OnResponseSignIn = context =>
{
context.Properties.ExpiresUtc = clock.UtcNow.Add(TimeSpan.FromMinutes(5));
}
};
}, SignInAsAlice);
Transaction transaction1 = await SendAsync(server, "http://example.com/testpath");
Transaction transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
clock.Add(TimeSpan.FromMinutes(3));
Transaction transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
clock.Add(TimeSpan.FromMinutes(3));
Transaction transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
transaction2.SetCookie.ShouldBe(null);
FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice");
transaction3.SetCookie.ShouldBe(null);
FindClaimValue(transaction3, ClaimTypes.Name).ShouldBe("Alice");
transaction4.SetCookie.ShouldBe(null);
FindClaimValue(transaction4, ClaimTypes.Name).ShouldBe(null);
}
[Fact]
public async Task CookieIsRenewedWithSlidingExpiration()
{
var clock = new TestClock();
TestServer server = CreateServer(options =>
{
options.SystemClock = clock;
options.ExpireTimeSpan = TimeSpan.FromMinutes(10);
options.SlidingExpiration = true;
}, SignInAsAlice);
Transaction transaction1 = await SendAsync(server, "http://example.com/testpath");
Transaction transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
clock.Add(TimeSpan.FromMinutes(4));
Transaction transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
clock.Add(TimeSpan.FromMinutes(4));
// transaction4 should arrive with a new SetCookie value
Transaction transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
clock.Add(TimeSpan.FromMinutes(4));
Transaction transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue);
transaction2.SetCookie.ShouldBe(null);
FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice");
transaction3.SetCookie.ShouldBe(null);
FindClaimValue(transaction3, ClaimTypes.Name).ShouldBe("Alice");
transaction4.SetCookie.ShouldNotBe(null);
FindClaimValue(transaction4, ClaimTypes.Name).ShouldBe("Alice");
transaction5.SetCookie.ShouldBe(null);
FindClaimValue(transaction5, ClaimTypes.Name).ShouldBe("Alice");
}
[Fact]
public async Task AjaxRedirectsAsExtraHeaderOnTwoHundred()
{
TestServer server = CreateServer(options =>
{
options.LoginPath = new PathString("/login");
});
Transaction transaction = await SendAsync(server, "http://example.com/protected", ajaxRequest: true);
transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK);
var responded = transaction.Response.Headers.GetValues("X-Responded-JSON");
responded.Count().ShouldBe(1);
responded.Single().ShouldContain("\"location\"");
}
private static string FindClaimValue(Transaction transaction, string claimType)
{
XElement claim = transaction.ResponseElement.Elements("claim").SingleOrDefault(elt => elt.Attribute("type").Value == claimType);
if (claim == null)
{
return null;
}
return claim.Attribute("value").Value;
}
private static async Task<XElement> GetAuthData(TestServer server, string url, string cookie)
{
var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Add("Cookie", cookie);
HttpResponseMessage response2 = await server.CreateClient().SendAsync(request);
string text = await response2.Content.ReadAsStringAsync();
XElement me = XElement.Parse(text);
return me;
}
private static TestServer CreateServer(Action<CookieAuthenticationOptions> configureOptions, Func<HttpContext, Task> testpath = null)
{
return TestServer.Create(app =>
{
app.UseServices(services => services.Add(DataProtectionServices.GetDefaultServices()));
app.UseCookieAuthentication(configureOptions);
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("/protected"))
{
res.StatusCode = 401;
}
else if (req.Path == new PathString("/protected/CustomRedirect"))
{
context.Response.Challenge(new AuthenticationProperties() { RedirectUri = "/CustomRedirect" });
}
else if (req.Path == new PathString("/me"))
{
Describe(res, new AuthenticationResult(context.User.Identity, new AuthenticationProperties(), new AuthenticationDescription()));
}
else if (req.Path.StartsWithSegments(new PathString("/me"), out remainder))
{
var result = await context.AuthenticateAsync(remainder.Value.Substring(1));
Describe(res, result);
}
else if (req.Path == new PathString("/testpath") && testpath != null)
{
await testpath(context);
}
else
{
await next();
}
});
});
}
private static void Describe(HttpResponse res, AuthenticationResult result)
{
res.StatusCode = 200;
res.ContentType = "text/xml";
var xml = new XElement("xml");
if (result != null && result.Identity != null)
{
xml.Add(result.Identity.Claims.Select(claim => new XElement("claim", new XAttribute("type", claim.Type), new XAttribute("value", claim.Value))));
}
if (result != null && result.Properties != null)
{
xml.Add(result.Properties.Dictionary.Select(extra => new XElement("extra", new XAttribute("type", extra.Key), new XAttribute("value", extra.Value))));
}
using (var memory = new MemoryStream())
{
using (var writer = new XmlTextWriter(memory, Encoding.UTF8))
{
xml.WriteTo(writer);
}
res.Body.Write(memory.ToArray(), 0, memory.ToArray().Length);
}
}
private static async Task<Transaction> SendAsync(TestServer server, string uri, string cookieHeader = null, bool ajaxRequest = false)
{
var request = new HttpRequestMessage(HttpMethod.Get, uri);
if (!string.IsNullOrEmpty(cookieHeader))
{
request.Headers.Add("Cookie", cookieHeader);
}
if (ajaxRequest)
{
request.Headers.Add("X-Requested-With", "XMLHttpRequest");
}
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; }
}
}
}