aspnetcore/test/Microsoft.AspNetCore.Identi.../FunctionalTest.cs

391 lines
18 KiB
C#

// Copyright (c) .NET Foundation. 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.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.AspNetCore.Identity.Test;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.AspNetCore.Identity.InMemory
{
public class FunctionalTest
{
const string TestPassword = "1qaz!QAZ";
[Fact]
public void UseIdentityThrowsWithoutAddIdentity()
{
var builder = new WebHostBuilder()
.Configure(app => app.UseIdentity());
Assert.Throws<InvalidOperationException>(() => new TestServer(builder));
}
[Fact]
public async Task CanChangePasswordOptions()
{
var clock = new TestClock();
var server = CreateServer(services => services.Configure<IdentityOptions>(options =>
{
options.Password.RequireUppercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireDigit = false;
}));
var transaction1 = await SendAsync(server, "http://example.com/createSimple");
// Assert
Assert.Equal(HttpStatusCode.OK, transaction1.Response.StatusCode);
Assert.Null(transaction1.SetCookie);
}
[Fact]
public async Task CanCreateMeLoginAndCookieStopsWorkingAfterExpiration()
{
var clock = new TestClock();
var server = CreateServer(services => services.Configure<IdentityOptions>(options =>
{
options.Cookies.ApplicationCookie.SystemClock = clock;
options.Cookies.ApplicationCookie.ExpireTimeSpan = TimeSpan.FromMinutes(10);
options.Cookies.ApplicationCookie.SlidingExpiration = false;
}));
var transaction1 = await SendAsync(server, "http://example.com/createMe");
Assert.Equal(HttpStatusCode.OK, transaction1.Response.StatusCode);
Assert.Null(transaction1.SetCookie);
var transaction2 = await SendAsync(server, "http://example.com/pwdLogin/false");
Assert.Equal(HttpStatusCode.OK, transaction2.Response.StatusCode);
Assert.NotNull(transaction2.SetCookie);
Assert.DoesNotContain("; expires=", transaction2.SetCookie);
var transaction3 = await SendAsync(server, "http://example.com/me", transaction2.CookieNameValue);
Assert.Equal("hao", FindClaimValue(transaction3, ClaimTypes.Name));
Assert.Null(transaction3.SetCookie);
clock.Add(TimeSpan.FromMinutes(7));
var transaction4 = await SendAsync(server, "http://example.com/me", transaction2.CookieNameValue);
Assert.Equal("hao", FindClaimValue(transaction4, ClaimTypes.Name));
Assert.Null(transaction4.SetCookie);
clock.Add(TimeSpan.FromMinutes(7));
var transaction5 = await SendAsync(server, "http://example.com/me", transaction2.CookieNameValue);
Assert.Null(FindClaimValue(transaction5, ClaimTypes.Name));
Assert.Null(transaction5.SetCookie);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task CanCreateMeLoginAndSecurityStampExtendsExpiration(bool rememberMe)
{
var clock = new TestClock();
var server = CreateServer(services => services.Configure<IdentityOptions>(options =>
{
options.Cookies.ApplicationCookie.SystemClock = clock;
}));
var transaction1 = await SendAsync(server, "http://example.com/createMe");
Assert.Equal(HttpStatusCode.OK, transaction1.Response.StatusCode);
Assert.Null(transaction1.SetCookie);
var transaction2 = await SendAsync(server, "http://example.com/pwdLogin/" + rememberMe);
Assert.Equal(HttpStatusCode.OK, transaction2.Response.StatusCode);
Assert.NotNull(transaction2.SetCookie);
if (rememberMe)
{
Assert.Contains("; expires=", transaction2.SetCookie);
}
else
{
Assert.DoesNotContain("; expires=", transaction2.SetCookie);
}
var transaction3 = await SendAsync(server, "http://example.com/me", transaction2.CookieNameValue);
Assert.Equal("hao", FindClaimValue(transaction3, ClaimTypes.Name));
Assert.Null(transaction3.SetCookie);
// Make sure we don't get a new cookie yet
clock.Add(TimeSpan.FromMinutes(10));
var transaction4 = await SendAsync(server, "http://example.com/me", transaction2.CookieNameValue);
Assert.Equal("hao", FindClaimValue(transaction4, ClaimTypes.Name));
Assert.Null(transaction4.SetCookie);
// Go past SecurityStampValidation interval and ensure we get a new cookie
clock.Add(TimeSpan.FromMinutes(21));
var transaction5 = await SendAsync(server, "http://example.com/me", transaction2.CookieNameValue);
Assert.NotNull(transaction5.SetCookie);
Assert.Equal("hao", FindClaimValue(transaction5, ClaimTypes.Name));
// Make sure new cookie is valid
var transaction6 = await SendAsync(server, "http://example.com/me", transaction5.CookieNameValue);
Assert.Equal("hao", FindClaimValue(transaction6, ClaimTypes.Name));
}
[Fact]
public async Task CanAccessOldPrincipalDuringSecurityStampReplacement()
{
var clock = new TestClock();
var server = CreateServer(services => services.Configure<IdentityOptions>(options =>
{
options.Cookies.ApplicationCookie.SystemClock = clock;
options.OnSecurityStampRefreshingPrincipal = c =>
{
var newId = new ClaimsIdentity();
newId.AddClaim(new Claim("PreviousName", c.CurrentPrincipal.Identity.Name));
c.NewPrincipal.AddIdentity(newId);
return Task.FromResult(0);
};
}));
var transaction1 = await SendAsync(server, "http://example.com/createMe");
Assert.Equal(HttpStatusCode.OK, transaction1.Response.StatusCode);
Assert.Null(transaction1.SetCookie);
var transaction2 = await SendAsync(server, "http://example.com/pwdLogin/false" );
Assert.Equal(HttpStatusCode.OK, transaction2.Response.StatusCode);
Assert.NotNull(transaction2.SetCookie);
Assert.DoesNotContain("; expires=", transaction2.SetCookie);
var transaction3 = await SendAsync(server, "http://example.com/me", transaction2.CookieNameValue);
Assert.Equal("hao", FindClaimValue(transaction3, ClaimTypes.Name));
Assert.Null(transaction3.SetCookie);
// Make sure we don't get a new cookie yet
clock.Add(TimeSpan.FromMinutes(10));
var transaction4 = await SendAsync(server, "http://example.com/me", transaction2.CookieNameValue);
Assert.Equal("hao", FindClaimValue(transaction4, ClaimTypes.Name));
Assert.Null(transaction4.SetCookie);
// Go past SecurityStampValidation interval and ensure we get a new cookie
clock.Add(TimeSpan.FromMinutes(21));
var transaction5 = await SendAsync(server, "http://example.com/me", transaction2.CookieNameValue);
Assert.NotNull(transaction5.SetCookie);
Assert.Equal("hao", FindClaimValue(transaction5, ClaimTypes.Name));
Assert.Equal("hao", FindClaimValue(transaction5, "PreviousName"));
// Make sure new cookie is valid
var transaction6 = await SendAsync(server, "http://example.com/me", transaction5.CookieNameValue);
Assert.Equal("hao", FindClaimValue(transaction6, ClaimTypes.Name));
}
[Fact]
public async Task TwoFactorRememberCookieVerification()
{
var server = CreateServer();
var transaction1 = await SendAsync(server, "http://example.com/createMe");
Assert.Equal(HttpStatusCode.OK, transaction1.Response.StatusCode);
Assert.Null(transaction1.SetCookie);
var transaction2 = await SendAsync(server, "http://example.com/twofactorRememeber");
Assert.Equal(HttpStatusCode.OK, transaction2.Response.StatusCode);
var setCookie = transaction2.SetCookie;
Assert.Contains(new IdentityCookieOptions().TwoFactorRememberMeCookieAuthenticationScheme + "=", setCookie);
Assert.Contains("; expires=", setCookie);
var transaction3 = await SendAsync(server, "http://example.com/isTwoFactorRememebered", transaction2.CookieNameValue);
Assert.Equal(HttpStatusCode.OK, transaction3.Response.StatusCode);
}
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 async Task<XElement> GetAuthData(TestServer server, string url, string cookie)
{
var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Add("Cookie", cookie);
var response2 = await server.CreateClient().SendAsync(request);
var text = await response2.Content.ReadAsStringAsync();
var me = XElement.Parse(text);
return me;
}
private static TestServer CreateServer(Action<IServiceCollection> configureServices = null, Func<HttpContext, Task> testpath = null, Uri baseAddress = null)
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseIdentity();
app.Use(async (context, next) =>
{
var req = context.Request;
var res = context.Response;
var userManager = context.RequestServices.GetRequiredService<UserManager<TestUser>>();
var signInManager = context.RequestServices.GetRequiredService<SignInManager<TestUser>>();
PathString remainder;
if (req.Path == new PathString("/normal"))
{
res.StatusCode = 200;
}
else if (req.Path == new PathString("/createMe"))
{
var result = await userManager.CreateAsync(new TestUser("hao"), TestPassword);
res.StatusCode = result.Succeeded ? 200 : 500;
}
else if (req.Path == new PathString("/createSimple"))
{
var result = await userManager.CreateAsync(new TestUser("simple"), "aaaaaa");
res.StatusCode = result.Succeeded ? 200 : 500;
}
else if (req.Path == new PathString("/protected"))
{
res.StatusCode = 401;
}
else if (req.Path.StartsWithSegments(new PathString("/pwdLogin"), out remainder))
{
var isPersistent = bool.Parse(remainder.Value.Substring(1));
var result = await signInManager.PasswordSignInAsync("hao", TestPassword, isPersistent, false);
res.StatusCode = result.Succeeded ? 200 : 500;
}
else if (req.Path == new PathString("/twofactorRememeber"))
{
var user = await userManager.FindByNameAsync("hao");
await signInManager.RememberTwoFactorClientAsync(user);
res.StatusCode = 200;
}
else if (req.Path == new PathString("/isTwoFactorRememebered"))
{
var user = await userManager.FindByNameAsync("hao");
var result = await signInManager.IsTwoFactorClientRememberedAsync(user);
res.StatusCode = result ? 200 : 500;
}
else if (req.Path == new PathString("/twofactorSignIn"))
{
}
else if (req.Path == new PathString("/me"))
{
var auth = new AuthenticateContext("Application");
auth.Authenticated(context.User, new AuthenticationProperties().Items, new AuthenticationDescription().Items);
Describe(res, auth);
}
else if (req.Path.StartsWithSegments(new PathString("/me"), out remainder))
{
var auth = new AuthenticateContext(remainder.Value.Substring(1));
await context.Authentication.AuthenticateAsync(auth);
Describe(res, auth);
}
else if (req.Path == new PathString("/testpath") && testpath != null)
{
await testpath(context);
}
else
{
await next();
}
});
})
.ConfigureServices(services =>
{
services.AddIdentity<TestUser, TestRole>();
services.AddSingleton<IUserStore<TestUser>, InMemoryStore<TestUser, TestRole>>();
services.AddSingleton<IRoleStore<TestRole>, InMemoryStore<TestUser, TestRole>>();
if (configureServices != null)
{
configureServices(services);
}
});
var server = new TestServer(builder);
server.BaseAddress = baseAddress;
return server;
}
private static void Describe(HttpResponse res, AuthenticateContext result)
{
res.StatusCode = 200;
res.ContentType = "text/xml";
var xml = new XElement("xml");
if (result != null && result.Principal != null)
{
xml.Add(result.Principal.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.Select(extra => new XElement("extra", new XAttribute("type", extra.Key), new XAttribute("value", extra.Value))));
}
using (var memory = new MemoryStream())
{
using (var writer = XmlWriter.Create(memory, new XmlWriterSettings { Encoding = 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; }
}
}
}