diff --git a/Directory.Build.props b/Directory.Build.props index b96a9dd..6254afd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,7 +8,7 @@ - 6.0.22 + 7.0.11 alpha diff --git a/WeixinAuth.sln b/WeixinAuth.sln index 1a5d6ae..25de876 100644 --- a/WeixinAuth.sln +++ b/WeixinAuth.sln @@ -28,6 +28,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WeixinAuth.UnitTest_3_1", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WeixinAuth.UnitTest_5_0", "test\WeixinAuth.UnitTest_5_0\WeixinAuth.UnitTest_5_0.csproj", "{B812C99A-70DF-4EB6-95B6-E3B8CECE5A3A}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QQConnect.UnitTest_6_0", "..\Myvas.AspNetCore.Authentication.QQConnect\test\QQConnect.UnitTest_6_0\QQConnect.UnitTest_6_0.csproj", "{5198DC01-2C6E-461C-A051-E9F0AF1A8640}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -50,6 +52,10 @@ Global {B812C99A-70DF-4EB6-95B6-E3B8CECE5A3A}.Debug|Any CPU.Build.0 = Debug|Any CPU {B812C99A-70DF-4EB6-95B6-E3B8CECE5A3A}.Release|Any CPU.ActiveCfg = Release|Any CPU {B812C99A-70DF-4EB6-95B6-E3B8CECE5A3A}.Release|Any CPU.Build.0 = Release|Any CPU + {5198DC01-2C6E-461C-A051-E9F0AF1A8640}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5198DC01-2C6E-461C-A051-E9F0AF1A8640}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5198DC01-2C6E-461C-A051-E9F0AF1A8640}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5198DC01-2C6E-461C-A051-E9F0AF1A8640}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -59,6 +65,7 @@ Global {94ABBE67-3755-4DD1-A25E-2407FB32C60E} = {73FCFCF4-3A1C-4D4D-939A-9CABDC2341DC} {688F627F-FF2D-4FE4-BAA7-BD94C139798B} = {73FCFCF4-3A1C-4D4D-939A-9CABDC2341DC} {B812C99A-70DF-4EB6-95B6-E3B8CECE5A3A} = {73FCFCF4-3A1C-4D4D-939A-9CABDC2341DC} + {5198DC01-2C6E-461C-A051-E9F0AF1A8640} = {73FCFCF4-3A1C-4D4D-939A-9CABDC2341DC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2AEDFD1F-BBE1-4727-9978-2FB04DCE84AF} diff --git a/global.json b/global.json index 88f12e1..9e390e5 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "6.0.100", + "version": "7.0.100", "rollForward": "latestFeature" } } \ No newline at end of file diff --git a/src/WeixinAuth/Helpers/CompressionExtensions.cs b/src/WeixinAuth/Helpers/CompressionExtensions.cs index 8e76f5b..6c16207 100644 --- a/src/WeixinAuth/Helpers/CompressionExtensions.cs +++ b/src/WeixinAuth/Helpers/CompressionExtensions.cs @@ -1,93 +1,86 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Runtime.Serialization.Formatters.Binary; -using System.Text; -using System.Threading.Tasks; - -namespace Myvas.AspNetCore.Authentication.WeixinAuth.Internal -{ - /// - /// ref. https://stackoverflow.com/questions/7343465/compression-decompression-string-with-c-sharp - /// - static internal class CompressionExtensions - { - public static async Task> Zip(this object obj) - { - byte[] bytes = obj.Serialize(); - - using (MemoryStream msi = new MemoryStream(bytes)) - using (MemoryStream mso = new MemoryStream()) - { - using (var gs = new GZipStream(mso, CompressionMode.Compress)) - await msi.CopyToAsync(gs); - - return mso.ToArray().AsEnumerable(); - } - } - - public static async Task Unzip(this byte[] bytes) - { - using (MemoryStream msi = new MemoryStream(bytes)) - using (MemoryStream mso = new MemoryStream()) - { - using (var gs = new GZipStream(msi, CompressionMode.Decompress)) - { - //gs.CopyTo(mso); - await gs.CopyToAsync(mso); - } - - return mso.ToArray().Deserialize(); - } - } - } - - internal static class SerializerExtensions - { - /// - /// Writes the given object instance to a binary file. - /// Object type (and all child types) must be decorated with the [Serializable] attribute. - /// To prevent a variable from being serialized, decorate it with the [NonSerialized] attribute; cannot be applied to properties. - /// - /// The type of object being written to the XML file. - /// The file path to write the object instance to. - /// The object instance to write to the XML file. - /// If false the file will be overwritten if it already exists. If true the contents will be appended to the file. - public static byte[] Serialize(this T objectToWrite) - { - using (MemoryStream stream = new MemoryStream()) - { - BinaryFormatter binaryFormatter = new BinaryFormatter(); - binaryFormatter.Serialize(stream, objectToWrite); - - return stream.GetBuffer(); - } - } - - /// - /// Reads an object instance from a binary file. - /// - /// The type of object to read from the XML. - /// The file path to read the object instance from. - /// Returns a new instance of the object read from the binary file. - public static async Task _Deserialize(this byte[] arr) - { - using (MemoryStream stream = new MemoryStream()) - { - BinaryFormatter binaryFormatter = new BinaryFormatter(); - await stream.WriteAsync(arr, 0, arr.Length); - stream.Position = 0; - - return (T)binaryFormatter.Deserialize(stream); - } - } - - public static async Task Deserialize(this byte[] arr) - { - object obj = await arr._Deserialize(); - return obj; - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Runtime.Serialization.Formatters.Binary; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Serialization; + +namespace Myvas.AspNetCore.Authentication.WeixinAuth.Internal +{ + /// + /// ref. https://stackoverflow.com/questions/7343465/compression-decompression-string-with-c-sharp + /// + static internal class CompressionExtensions + { + public static async Task> Zip(this object obj) + { + byte[] bytes = obj.Serialize(); + + using (MemoryStream msi = new MemoryStream(bytes)) + using (MemoryStream mso = new MemoryStream()) + { + using (var gs = new GZipStream(mso, CompressionMode.Compress)) + await msi.CopyToAsync(gs); + + return mso.ToArray().AsEnumerable(); + } + } + + public static async Task Unzip(this byte[] bytes) + { + using (MemoryStream msi = new MemoryStream(bytes)) + using (MemoryStream mso = new MemoryStream()) + { + using (var gs = new GZipStream(msi, CompressionMode.Decompress)) + { + //gs.CopyTo(mso); + await gs.CopyToAsync(mso); + } + + return mso.ToArray().Deserialize(); + } + } + } + + internal static class SerializerExtensions + { + /// + /// Writes the given object instance to a binary file. + /// Object type (and all child types) must be decorated with the [Serializable] attribute. + /// To prevent a variable from being serialized, decorate it with the [NonSerialized] attribute; cannot be applied to properties. + /// + /// The type of object being written to the XML file. + /// The file path to write the object instance to. + /// The object instance to write to the XML file. + /// If false the file will be overwritten if it already exists. If true the contents will be appended to the file. + public static byte[] Serialize(this T objectToWrite) + { + using var stream = new MemoryStream(); + var serializer = new XmlSerializer(typeof(T)); + serializer.Serialize(stream, objectToWrite); + return stream.GetBuffer(); + } + + /// + /// Reads an object instance from a binary file. + /// + /// The type of object to read from the XML. + /// The file path to read the object instance from. + /// Returns a new instance of the object read from the binary file. + public static async Task _Deserialize(this byte[] arr) + { + using var stream = new MemoryStream(arr); + var serializer = new XmlSerializer(typeof(T)); + return await Task.FromResult((T)serializer.Deserialize(stream)); + } + + public static async Task Deserialize(this byte[] arr) + { + object obj = await arr._Deserialize(); + return obj; + } + } +} diff --git a/src/WeixinAuth/WeixinAuth.csproj b/src/WeixinAuth/WeixinAuth.csproj index f2eb65f..2bcbded 100644 --- a/src/WeixinAuth/WeixinAuth.csproj +++ b/src/WeixinAuth/WeixinAuth.csproj @@ -1,6 +1,6 @@  - net6.0;net5.0;netcoreapp3.1 + net7.0;net6.0;net5.0;netcoreapp3.1 enable Myvas.AspNetCore.Authentication.WeixinAuth @@ -18,6 +18,10 @@ Myvas.AspNetCore.Authentication + + + + diff --git a/test/WeixinAuth.UnitTest/WeixinAuth.UnitTest.csproj b/test/WeixinAuth.UnitTest/WeixinAuth.UnitTest.csproj index e768990..dad59bd 100644 --- a/test/WeixinAuth.UnitTest/WeixinAuth.UnitTest.csproj +++ b/test/WeixinAuth.UnitTest/WeixinAuth.UnitTest.csproj @@ -1,16 +1,16 @@  - net6.0 + net7.0 false - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/WeixinAuth.UnitTest/WeixinAuthTests.cs b/test/WeixinAuth.UnitTest/WeixinAuthTests.cs index e37a194..b979463 100644 --- a/test/WeixinAuth.UnitTest/WeixinAuthTests.cs +++ b/test/WeixinAuth.UnitTest/WeixinAuthTests.cs @@ -736,7 +736,7 @@ public async Task ChallengeWillTriggerApplyRedirectEvent() } [Fact] - public async Task AuthenticateWithoutCookieWillFail() + public async Task AuthenticateWithoutCookieWillReturnNone() { var server = CreateServer(o => { @@ -748,9 +748,10 @@ public async Task AuthenticateWithoutCookieWillFail() var res = context.Response; if (req.Path == new PathString("/auth")) { - var result = await context.AuthenticateAsync("WeixinAuth"); - Assert.NotNull(result.Failure); - } + var result = await context.AuthenticateAsync("WeixinAuth"); + //Assert.NotNull(result.Failure); + Assert.True(result.None); + } }); var transaction = await server.SendAsync("https://example.com/auth"); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); diff --git a/test/WeixinAuth.UnitTest_3_1/WeixinAuth.UnitTest_3_1.csproj b/test/WeixinAuth.UnitTest_3_1/WeixinAuth.UnitTest_3_1.csproj index ac68c23..2caf554 100644 --- a/test/WeixinAuth.UnitTest_3_1/WeixinAuth.UnitTest_3_1.csproj +++ b/test/WeixinAuth.UnitTest_3_1/WeixinAuth.UnitTest_3_1.csproj @@ -6,10 +6,10 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/WeixinAuth.UnitTest_5_0/WeixinAuth.UnitTest_5_0.csproj b/test/WeixinAuth.UnitTest_5_0/WeixinAuth.UnitTest_5_0.csproj index 7c5b3c0..f93e8cd 100644 --- a/test/WeixinAuth.UnitTest_5_0/WeixinAuth.UnitTest_5_0.csproj +++ b/test/WeixinAuth.UnitTest_5_0/WeixinAuth.UnitTest_5_0.csproj @@ -7,11 +7,10 @@ - - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/WeixinAuth.UnitTest_6_0/TestServers/TestExtensions.cs b/test/WeixinAuth.UnitTest_6_0/TestServers/TestExtensions.cs new file mode 100644 index 0000000..77dbcc9 --- /dev/null +++ b/test/WeixinAuth.UnitTest_6_0/TestServers/TestExtensions.cs @@ -0,0 +1,91 @@ +// 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.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +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.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; + +namespace UnitTest +{ + public static class TestExtensions + { + public const string CookieAuthenticationScheme = "External"; + + public static async Task SendAsync(this TestServer server, string uri, params string[] cookies) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + if (cookies != null && cookies.Count() > 0) + { + request.Headers.Add("Cookie", string.Join("; ", cookies)); + } + + var transaction = new TestTransaction + { + Request = request, + Response = await server.CreateClient().SendAsync(request), + }; + + if (transaction.Response.Headers.Contains("Set-Cookie")) + { + transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList(); + } + 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; + } + + public static Task DescribeAsync(this HttpResponse res, ClaimsPrincipal principal) + { + res.StatusCode = 200; + res.ContentType = "text/xml"; + var xml = new XElement("xml"); + if (principal != null) + { + foreach (var identity in principal.Identities) + { + xml.Add(identity.Claims.Select(claim => + new XElement("claim", new XAttribute("type", claim.Type), + new XAttribute("value", claim.Value), + new XAttribute("issuer", claim.Issuer)))); + } + } + var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString()); + return res.Body.WriteAsync(xmlBytes, 0, xmlBytes.Length); + } + + public static Task DescribeAsync(this HttpResponse res, IEnumerable tokens) + { + res.StatusCode = 200; + res.ContentType = "text/xml"; + var xml = new XElement("xml"); + if (tokens != null) + { + foreach (var token in tokens) + { + xml.Add(new XElement("token", new XAttribute("name", token.Name), + new XAttribute("value", token.Value))); + } + } + var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString()); + return res.Body.WriteAsync(xmlBytes, 0, xmlBytes.Length); + } + + } +} diff --git a/test/WeixinAuth.UnitTest_6_0/TestServers/TestHandlers.cs b/test/WeixinAuth.UnitTest_6_0/TestServers/TestHandlers.cs new file mode 100644 index 0000000..ca7dbf0 --- /dev/null +++ b/test/WeixinAuth.UnitTest_6_0/TestServers/TestHandlers.cs @@ -0,0 +1,116 @@ +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. + +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace UnitTest +{ + public class TestAuthHandler : AuthenticationHandler, IAuthenticationSignInHandler + { + public TestAuthHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + { } + + public int SignInCount { get; set; } + public int SignOutCount { get; set; } + public int ForbidCount { get; set; } + public int ChallengeCount { get; set; } + public int AuthenticateCount { get; set; } + + protected override Task HandleChallengeAsync(AuthenticationProperties properties) + { + ChallengeCount++; + return Task.CompletedTask; + } + + protected override Task HandleForbiddenAsync(AuthenticationProperties properties) + { + ForbidCount++; + return Task.CompletedTask; + } + + protected override Task HandleAuthenticateAsync() + { + AuthenticateCount++; + var principal = new ClaimsPrincipal(); + var id = new ClaimsIdentity(); + id.AddClaim(new Claim(ClaimTypes.NameIdentifier, Scheme.Name, ClaimValueTypes.String, Scheme.Name)); + principal.AddIdentity(id); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name))); + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + SignInCount++; + return Task.CompletedTask; + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + SignOutCount++; + return Task.CompletedTask; + } + } + + public class TestHandler : IAuthenticationSignInHandler + { + public AuthenticationScheme Scheme { get; set; } + public int SignInCount { get; set; } + public int SignOutCount { get; set; } + public int ForbidCount { get; set; } + public int ChallengeCount { get; set; } + public int AuthenticateCount { get; set; } + + public Task AuthenticateAsync() + { + AuthenticateCount++; + var principal = new ClaimsPrincipal(); + var id = new ClaimsIdentity(); + id.AddClaim(new Claim(ClaimTypes.NameIdentifier, Scheme.Name, ClaimValueTypes.String, Scheme.Name)); + principal.AddIdentity(id); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name))); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + ChallengeCount++; + return Task.CompletedTask; + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + ForbidCount++; + return Task.CompletedTask; + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + Scheme = scheme; + return Task.CompletedTask; + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + SignInCount++; + return Task.CompletedTask; + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + SignOutCount++; + return Task.CompletedTask; + } + } + + public class TestHandler2 : TestHandler + { + } + + public class TestHandler3 : TestHandler + { + } +} \ No newline at end of file diff --git a/test/WeixinAuth.UnitTest_6_0/TestServers/TestHttpMessageHandler.cs b/test/WeixinAuth.UnitTest_6_0/TestServers/TestHttpMessageHandler.cs new file mode 100644 index 0000000..d3193c3 --- /dev/null +++ b/test/WeixinAuth.UnitTest_6_0/TestServers/TestHttpMessageHandler.cs @@ -0,0 +1,24 @@ +// 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.Net.Http; +using System.Threading.Tasks; + +namespace UnitTest +{ + public class TestHttpMessageHandler : HttpMessageHandler + { + public Func Sender { get; set; } + + protected override Task SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) + { + if (Sender != null) + { + return Task.FromResult(Sender(request)); + } + + return Task.FromResult(null); + } + } +} diff --git a/test/WeixinAuth.UnitTest_6_0/TestServers/TestServerBuilder.cs b/test/WeixinAuth.UnitTest_6_0/TestServers/TestServerBuilder.cs new file mode 100644 index 0000000..82fc6c0 --- /dev/null +++ b/test/WeixinAuth.UnitTest_6_0/TestServers/TestServerBuilder.cs @@ -0,0 +1,107 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Myvas.AspNetCore.Authentication; +using System; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace UnitTest.TestServers +{ + internal class TestServerBuilder + { + public static readonly string DefaultAuthority = @"https://login.microsoftonline.com/common"; + public static readonly string TestHost = @"https://example.com"; + public static readonly string Challenge = "/challenge"; + public static readonly string ChallengeWithOutContext = "/challengeWithOutContext"; + public static readonly string ChallengeWithProperties = "/challengeWithProperties"; + public static readonly string Signin = "/signin"; + public static readonly string Signout = "/signout"; + + public static WeixinAuthOptions CreateWeixinOpenOptions() => + new WeixinAuthOptions + { + AppId = "Test Id", + AppSecret = "Test Secret" + //o.SignInScheme = "auth1";//WeixinOpenDefaults.AuthenticationScheme + }; + + public static WeixinAuthOptions CreateWeixinOpenOptions(Action update) + { + var options = CreateWeixinOpenOptions(); + update?.Invoke(options); + return options; + } + + public static TestServer CreateServer(Action options) + { + return CreateServer(options, handler: null, properties: null); + } + + public static TestServer CreateServer( + Action options, + Func handler, + AuthenticationProperties properties) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + + if (req.Path == new PathString(Challenge)) + { + await context.ChallengeAsync(WeixinAuthDefaults.AuthenticationScheme); + } + else if (req.Path == new PathString(ChallengeWithProperties)) + { + await context.ChallengeAsync(WeixinAuthDefaults.AuthenticationScheme, properties); + } + else if (req.Path == new PathString(ChallengeWithOutContext)) + { + res.StatusCode = 401; + } + else if (req.Path == new PathString(Signin)) + { + await context.SignInAsync(WeixinAuthDefaults.AuthenticationScheme, new ClaimsPrincipal()); + } + else if (req.Path == new PathString(Signout)) + { + await context.SignOutAsync(WeixinAuthDefaults.AuthenticationScheme); + } + else if (req.Path == new PathString("/signout_with_specific_redirect_uri")) + { + await context.SignOutAsync( + WeixinAuthDefaults.AuthenticationScheme, + new AuthenticationProperties() { RedirectUri = "http://www.example.com/specific_redirect_uri" }); + } + else if (handler != null) + { + await handler(context); + } + else + { + await next(); + } + }); + }) + .ConfigureServices(services => + { + services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie() + .AddWeixinAuth(options); + }); + + return new TestServer(builder); + } + } + + +} diff --git a/test/WeixinAuth.UnitTest_6_0/TestServers/TestTransaction.cs b/test/WeixinAuth.UnitTest_6_0/TestServers/TestTransaction.cs new file mode 100644 index 0000000..eb180a8 --- /dev/null +++ b/test/WeixinAuth.UnitTest_6_0/TestServers/TestTransaction.cs @@ -0,0 +1,62 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Xml.Linq; + +namespace UnitTest +{ + public class TestTransaction + { + public HttpRequestMessage Request { get; set; } + public HttpResponseMessage Response { get; set; } + + public IList SetCookie { get; set; } + + public string ResponseText { get; set; } + public XElement ResponseElement { get; set; } + + public string AuthenticationCookieValue + { + get + { + if (SetCookie != null && SetCookie.Count > 0) + { + var authCookie = SetCookie.SingleOrDefault(c => c.Contains(".AspNetCore." + TestExtensions.CookieAuthenticationScheme + "=")); + if (authCookie != null) + { + return authCookie.Substring(0, authCookie.IndexOf(';')); + } + } + + return null; + } + } + + public string FindClaimValue(string claimType, string issuer = null) + { + var claim = ResponseElement.Elements("claim") + .SingleOrDefault(elt => elt.Attribute("type").Value == claimType && + (issuer == null || elt.Attribute("issuer").Value == issuer)); + if (claim == null) + { + return null; + } + return claim.Attribute("value").Value; + } + + public string FindTokenValue(string name) + { + var claim = ResponseElement.Elements("token") + .SingleOrDefault(elt => elt.Attribute("name").Value == name); + if (claim == null) + { + return null; + } + return claim.Attribute("value").Value; + } + + } +} diff --git a/test/WeixinAuth.UnitTest_6_0/WeixinAuth.UnitTest_6_0.csproj b/test/WeixinAuth.UnitTest_6_0/WeixinAuth.UnitTest_6_0.csproj new file mode 100644 index 0000000..e768990 --- /dev/null +++ b/test/WeixinAuth.UnitTest_6_0/WeixinAuth.UnitTest_6_0.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + false + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/test/WeixinAuth.UnitTest_6_0/WeixinAuthTests.cs b/test/WeixinAuth.UnitTest_6_0/WeixinAuthTests.cs new file mode 100644 index 0000000..e37a194 --- /dev/null +++ b/test/WeixinAuth.UnitTest_6_0/WeixinAuthTests.cs @@ -0,0 +1,1560 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.Authentication.Twitter; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.TestHost; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Myvas.AspNetCore.Authentication; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Threading.Tasks; +using Xunit; + +namespace UnitTest +{ + public class WeixinAuthTests + { + string correlationKey = ".xsrf"; + string correlationId = "TestCorrelationId"; + string correlationMarker = "N"; + + private void ConfigureDefaults(WeixinAuthOptions o) + { + o.AppId = "Test Id"; + o.AppSecret = "Test Secret"; + //o.SignInScheme = "auth1";//WeixinAuthDefaults.AuthenticationScheme; + } + + [Fact] + public async Task CodeMockValid() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("WeixinAuthTest")); + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.StateDataFormat = stateFormat; + o.BackchannelHttpHandler = CreateBackchannel(); + o.Events = new OAuthEvents() + { + OnCreatingTicket = context => + { + Assert.True(context.User.ToString().Length > 0); + Assert.Equal("Test Access Token", context.AccessToken); + Assert.Equal("Test Refresh Token", context.RefreshToken); + Assert.Equal(TimeSpan.FromSeconds(3600), context.ExpiresIn); + Assert.Equal("Test Open ID", context.Identity.FindFirst(ClaimTypes.NameIdentifier)?.Value); + Assert.Equal("Test Name", context.Identity.FindFirst(ClaimTypes.Name)?.Value); + Assert.Equal("Test Open ID", context.Identity.FindFirst(WeixinAuthClaimTypes.OpenId)?.Value); + Assert.Equal("Test Union ID", context.Identity.FindFirst(WeixinAuthClaimTypes.UnionId)?.Value); + return Task.FromResult(0); + } + }; + }); + + // Skip the challenge step, go directly to the callback path + + var properties = new AuthenticationProperties(); + properties.Items.Add(correlationKey, correlationId); + properties.RedirectUri = "/Account/ExternalLoginCallback?returnUrl=%2FHome%2FUserInfo"; + var state = stateFormat.Protect(properties); + + var transaction = await server.SendAsync( + $"https://example.com/signin-{WeixinAuthDefaults.AuthenticationScheme}?code=TestCode&state={UrlEncoder.Default.Encode(correlationId)}", + $".AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationId}={correlationMarker}" + + $";.AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationMarker}.{correlationId}={state}"); + + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/Account/ExternalLoginCallback?returnUrl=%2FHome%2FUserInfo", transaction.Response.Headers.GetValues("Location").First()); + } + + [Fact] + public async Task CanForwardDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = WeixinAuthDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + }) + .AddWeixinAuth(o => + { + ConfigureDefaults(o); + o.SignInScheme = "auth1"; + o.ForwardDefault = "auth1"; + }); + + var forwardDefault = new TestHandler(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + + [Fact] + public async Task ForwardSignInThrows() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = WeixinAuthDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddWeixinAuth(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + } + + + [Fact] + public async Task ForwardSignOutThrows() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = WeixinAuthDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddWeixinAuth(o => + { + ConfigureDefaults(o); + o.ForwardDefault = "auth1"; + o.ForwardSignOut = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + } + + + [Fact] + public async Task ForwardForbidWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = WeixinAuthDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddWeixinAuth(o => + { + ConfigureDefaults(o); + o.SignInScheme = "auth1"; //Important! + o.ForwardDefault = "auth1"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ForbidAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(1, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + + [Fact] + public async Task ForwardAuthenticateWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication(o => + { + o.DefaultScheme = WeixinAuthDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("specific", "specific"); + }) + .AddWeixinAuth(o => + { + ConfigureDefaults(o); + o.SignInScheme = "auth1"; //Important! + o.ForwardDefault = "auth1"; + o.ForwardAuthenticate = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(1, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardChallengeWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = WeixinAuthDefaults.AuthenticationScheme; + o.AddScheme("specific", "specific"); + o.AddScheme("auth1", "auth1"); + }) + .AddWeixinAuth(o => + { + ConfigureDefaults(o); + o.SignInScheme = "auth1"; //Important! + o.ForwardDefault = "auth1"; + o.ForwardChallenge = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.ChallengeAsync(); + Assert.Equal(0, specific.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(1, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + } + + [Fact] + public async Task ForwardSelectorWinsOverDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = WeixinAuthDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddWeixinAuth(o => + { + ConfigureDefaults(o); + o.SignInScheme = "auth1"; //Important! + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, selector.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, selector.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, selector.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task NullForwardSelectorUsesDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = WeixinAuthDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddWeixinAuth(o => + { + ConfigureDefaults(o); + o.SignInScheme = "auth1"; //Important! + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => null; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, forwardDefault.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, forwardDefault.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, forwardDefault.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + Assert.Equal(0, specific.AuthenticateCount); + Assert.Equal(0, specific.ForbidCount); + Assert.Equal(0, specific.ChallengeCount); + Assert.Equal(0, specific.SignInCount); + Assert.Equal(0, specific.SignOutCount); + } + + [Fact] + public async Task SpecificForwardWinsOverSelectorAndDefault() + { + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication(o => + { + o.DefaultScheme = WeixinAuthDefaults.AuthenticationScheme; + o.AddScheme("auth1", "auth1"); + o.AddScheme("selector", "selector"); + o.AddScheme("specific", "specific"); + }) + .AddWeixinAuth(o => + { + ConfigureDefaults(o); + o.SignInScheme = "auth1"; //Important! + o.ForwardDefault = "auth1"; + o.ForwardDefaultSelector = _ => "selector"; + o.ForwardAuthenticate = "specific"; + o.ForwardChallenge = "specific"; + o.ForwardSignIn = "specific"; + o.ForwardSignOut = "specific"; + o.ForwardForbid = "specific"; + }); + + var specific = new TestHandler(); + services.AddSingleton(specific); + var forwardDefault = new TestHandler2(); + services.AddSingleton(forwardDefault); + var selector = new TestHandler3(); + services.AddSingleton(selector); + + var sp = services.BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = sp; + + await context.AuthenticateAsync(); + Assert.Equal(1, specific.AuthenticateCount); + + await context.ForbidAsync(); + Assert.Equal(1, specific.ForbidCount); + + await context.ChallengeAsync(); + Assert.Equal(1, specific.ChallengeCount); + + await Assert.ThrowsAsync(() => context.SignOutAsync()); + await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + + Assert.Equal(0, forwardDefault.AuthenticateCount); + Assert.Equal(0, forwardDefault.ForbidCount); + Assert.Equal(0, forwardDefault.ChallengeCount); + Assert.Equal(0, forwardDefault.SignInCount); + Assert.Equal(0, forwardDefault.SignOutCount); + Assert.Equal(0, selector.AuthenticateCount); + Assert.Equal(0, selector.ForbidCount); + Assert.Equal(0, selector.ChallengeCount); + Assert.Equal(0, selector.SignInCount); + Assert.Equal(0, selector.SignOutCount); + } + + [Fact] + public async Task VerifySignInSchemeCannotBeSetToSelf() + { + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.SignInScheme = WeixinAuthDefaults.AuthenticationScheme; + }); + var error = await Assert.ThrowsAsync(() => server.SendAsync("https://example.com/challenge")); + Assert.Contains("cannot be set to itself", error.Message); + } + + [Fact] + public async Task VerifySchemeDefaults() + { + var services = new ServiceCollection(); + services.AddAuthentication().AddWeixinAuth(); + var sp = services.BuildServiceProvider(); + var schemeProvider = sp.GetRequiredService(); + var scheme = await schemeProvider.GetSchemeAsync(WeixinAuthDefaults.AuthenticationScheme); + Assert.NotNull(scheme); + Assert.Equal("WeixinAuthHandler", scheme.HandlerType.Name); + Assert.Equal(WeixinAuthDefaults.AuthenticationScheme, scheme.DisplayName); + } + + [Fact] + public async Task ChallengeWillTriggerRedirection() + { + var server = CreateServer(o => + { + ConfigureDefaults(o); + }); + + var transaction = await server.SendAsync("https://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var location = transaction.Response.Headers.Location.ToString(); + Assert.StartsWith(WeixinAuthDefaults.AuthorizationEndpoint, location); + Assert.Contains("&redirect_uri=", location); + Assert.Contains("&response_type=code", location); + Assert.Contains("&scope=", location); + Assert.Contains("&state=", location); + Assert.Contains("#wechat_redirect", location); + + Assert.DoesNotContain("access_type=", location); + Assert.DoesNotContain("prompt=", location); + Assert.DoesNotContain("approval_prompt=", location); + Assert.DoesNotContain("login_hint=", location); + Assert.DoesNotContain("include_granted_scopes=", location); + } + + [Fact] + public async Task SignInThrows() + { + var server = CreateServer(o => + { + ConfigureDefaults(o); + }); + var transaction = await server.SendAsync("https://example.com/signin"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task SignOutThrows() + { + var server = CreateServer(o => + { + ConfigureDefaults(o); + }); + var transaction = await server.SendAsync("https://example.com/signout"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ForbidWillRedirect() + { + var server = CreateServer(o => + { + ConfigureDefaults(o); + }); + var transaction = await server.SendAsync("https://example.com/forbid"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + } + + [Fact] + public async Task Challenge401WillNotTriggerRedirection() + { + var server = CreateServer(o => + { + ConfigureDefaults(o); + }); + var transaction = await server.SendAsync("https://example.com/401"); + Assert.Equal(HttpStatusCode.Unauthorized, transaction.Response.StatusCode); + } + + [Fact] + public async Task ChallengeWillSetCorrelationCookie() + { + var server = CreateServer(o => + { + ConfigureDefaults(o); + }); + var transaction = await server.SendAsync("https://example.com/challenge"); + Assert.Contains(transaction.SetCookie, cookie => cookie.StartsWith(".AspNetCore.Correlation.WeixinAuth.")); + } + + [Fact] + public async Task ChallengeWillSetDefaultScope() + { + var server = CreateServer(o => + { + ConfigureDefaults(o); + }); + var transaction = await server.SendAsync("https://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var query = transaction.Response.Headers.Location.Query; + Assert.Contains("scope=", query); + } + + [Fact] + public async Task ChallengeWillUseAuthenticationPropertiesParametersAsQueryArguments() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("WeixinAuthTest")); + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.StateDataFormat = stateFormat; + }, + context => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge2")) + { + return context.ChallengeAsync("WeixinAuth", new OAuthChallengeProperties + { + Scope = new string[] { "snsapi_login", "https://www.googleapis.com/auth/plus.login" }, + }); + } + + return Task.FromResult(null); + }); + var transaction = await server.SendAsync("https://example.com/challenge2"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + + // verify query arguments + var query = QueryHelpers.ParseQuery(transaction.Response.Headers.Location.Query); + Assert.Equal("snsapi_login,https://www.googleapis.com/auth/plus.login", query["scope"]); + //Assert.Equal("test@example.com", query["login_hint"]); + + // verify that the passed items were not serialized + var state = query["state"]; + var stateProperties = WeixinAuthAuthenticationPropertiesHelper.GetByCorrelationId(stateFormat, transaction.SetCookie, state, WeixinAuthDefaults.AuthenticationScheme, ".AspNetCore.Correlation"); + Assert.DoesNotContain("scope", stateProperties.Items.Keys); + Assert.DoesNotContain("login_hint", stateProperties.Items.Keys); + } + + [Fact] + public async Task ChallengeWillUseAuthenticationPropertiesItemsAsParameters() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("WeixinAuthTest")); + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.StateDataFormat = stateFormat; + }, + context => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge2")) + { + return context.ChallengeAsync(WeixinAuthDefaults.AuthenticationScheme, new AuthenticationProperties(new Dictionary() + { + { "scope", "https://www.googleapis.com/auth/plus.login" }, + //{ "login_hint", "test@example.com" }, + })); + } + + return Task.FromResult(null); + }); + var transaction = await server.SendAsync("https://example.com/challenge2"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + + // verify query arguments + var query = QueryHelpers.ParseQuery(transaction.Response.Headers.Location.Query); + Assert.Equal("https://www.googleapis.com/auth/plus.login", query["scope"]); + + // verify that the passed items were not serialized + var state = query["state"]; + var stateProperties = WeixinAuthAuthenticationPropertiesHelper.GetByCorrelationId(stateFormat, transaction.SetCookie, state, WeixinAuthDefaults.AuthenticationScheme, ".AspNetCore.Correlation"); + Assert.DoesNotContain("scope", stateProperties.Items.Keys); + } + + [Fact] + public async Task ChallengeWillUseAuthenticationPropertiesItemsAsQueryArgumentsButParametersWillOverwrite() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("WeixinAuthTest")); + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.StateDataFormat = stateFormat; + }, + context => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge2")) + { + return context.ChallengeAsync("WeixinAuth", new OAuthChallengeProperties() { Scope = new string[] { "https://www.googleapis.com/auth/plus.login" } }); + } + + return Task.FromResult(null); + }); + var transaction = await server.SendAsync("https://example.com/challenge2"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + + // verify query arguments + var query = QueryHelpers.ParseQuery(transaction.Response.Headers.Location.Query); + Assert.Equal("https://www.googleapis.com/auth/plus.login", query[OAuthChallengeProperties.ScopeKey]); + + // verify that the passed items were not serialized + var state = query["state"]; + var stateProperties = WeixinAuthAuthenticationPropertiesHelper.GetByCorrelationId(stateFormat, transaction.SetCookie, state, WeixinAuthDefaults.AuthenticationScheme, ".AspNetCore.Correlation"); + Assert.Contains(".redirect", stateProperties.Items.Keys); + Assert.Contains(".xsrf", stateProperties.Items.Keys); + Assert.DoesNotContain("scope", stateProperties.Items.Keys); + } + + [Fact] + public async Task ChallengeWillTriggerApplyRedirectEvent() + { + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.Events = new OAuthEvents + { + OnRedirectToAuthorizationEndpoint = context => + { + var oldUri = new Uri(context.RedirectUri); + var queryBuilder = new QueryBuilder() + { + { "custom", "test" } + }; + var customUrl = o.AuthorizationEndpoint + oldUri.PathAndQuery + queryBuilder + "#wechat_redirect"; + context.Response.Redirect(customUrl); + return Task.FromResult(0); + } + }; + }); + var transaction = await server.SendAsync("https://example.com/challenge"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + var query = transaction.Response.Headers.Location.Query; + Assert.Contains("custom=test", query); + } + + [Fact] + public async Task AuthenticateWithoutCookieWillFail() + { + var server = CreateServer(o => + { + ConfigureDefaults(o); + }, + async context => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/auth")) + { + var result = await context.AuthenticateAsync("WeixinAuth"); + Assert.NotNull(result.Failure); + } + }); + var transaction = await server.SendAsync("https://example.com/auth"); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + } + + [Fact] + public async Task ReplyPathWithoutStateQueryStringWillBeRejected() + { + var server = CreateServer(o => + { + ConfigureDefaults(o); + }); + var error = await Assert.ThrowsAnyAsync(() => server.SendAsync("https://example.com/signin-WeixinAuth?code=TestCode")); + Assert.Equal("The oauth state was missing.", error.GetBaseException().Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReplyPathWithErrorFails(bool redirect) + { + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.StateDataFormat = new TestStateDataFormat(); + o.Events = redirect ? new OAuthEvents() + { + OnRemoteFailure = ctx => + { + ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); + ctx.HandleResponse(); + return Task.FromResult(0); + } + } : new OAuthEvents(); + }); + var sendTask = server.SendAsync("https://example.com/signin-weixinauth?error=OMG&error_description=SoBad&error_uri=foobar&state=protected_state", + ".AspNetCore.Correlation.WeixinAuth.corrilationId=N"); + if (redirect) + { + var transaction = await sendTask; + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/error?FailureMessage=OMG" + UrlEncoder.Default.Encode(";Description=SoBad;Uri=foobar"), transaction.Response.Headers.GetValues("Location").First()); + } + else + { + var error = await Assert.ThrowsAnyAsync(() => sendTask); + Assert.Equal("OMG;Description=SoBad;Uri=foobar", error.GetBaseException().Message); + } + } + + [Theory] + [InlineData(null)] + [InlineData("CustomIssuer")] + public async Task ReplyPathWillAuthenticateValidAuthorizeCodeAndState(string claimsIssuer) + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("WeixinAuthTest")); + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.SaveTokens = true; + o.StateDataFormat = stateFormat; + if (claimsIssuer != null) + { + o.ClaimsIssuer = claimsIssuer; + } + o.BackchannelHttpHandler = CreateBackchannel(); + }); + + var properties = new AuthenticationProperties(); + properties.Items.Add(correlationKey, correlationId); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + $"https://example.com/signin-{WeixinAuthDefaults.AuthenticationScheme}?code=TestCode&state={UrlEncoder.Default.Encode(correlationId)}", + $".AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationId}={correlationMarker}" + + $";.AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationMarker}.{correlationId}={state}"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.True(transaction.SetCookie.Count >= 2); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/me", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + var expectedIssuer = claimsIssuer ?? WeixinAuthDefaults.AuthenticationScheme; + Assert.Equal("Test Name", transaction.FindClaimValue(ClaimTypes.Name, expectedIssuer)); + Assert.Equal("Test Open ID", transaction.FindClaimValue(ClaimTypes.NameIdentifier, expectedIssuer)); + Assert.Equal("Test Open ID", transaction.FindClaimValue(WeixinAuthClaimTypes.OpenId, expectedIssuer)); + Assert.Equal("Test Union ID", transaction.FindClaimValue(WeixinAuthClaimTypes.UnionId, expectedIssuer)); + + // Ensure claims transformation + Assert.Equal("yup", transaction.FindClaimValue("xform")); + + transaction = await server.SendAsync("https://example.com/tokens", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.Equal("Test Access Token", transaction.FindTokenValue("access_token")); + //Assert.Equal("Bearer", transaction.FindTokenValue("token_type")); + Assert.NotNull(transaction.FindTokenValue("expires_at")); + } + + // REVIEW: Fix this once we revisit error handling to not blow up + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReplyPathWillThrowIfCodeIsInvalid(bool redirect) + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("WeixinAuthTest")); + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.StateDataFormat = stateFormat; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + return ReturnJsonResponse(new { Error = "Error" }, + HttpStatusCode.BadRequest); + } + }; + o.Events = redirect ? new OAuthEvents() + { + OnRemoteFailure = ctx => + { + ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); + ctx.HandleResponse(); + return Task.FromResult(0); + } + } : new OAuthEvents(); + }); + var properties = new AuthenticationProperties(); + properties.Items.Add(correlationKey, correlationId); + properties.RedirectUri = "/me"; + + var state = stateFormat.Protect(properties); + var sendTask = server.SendAsync( + $"https://example.com/signin-{WeixinAuthDefaults.AuthenticationScheme}?code=TestCode&state={UrlEncoder.Default.Encode(correlationId)}", + $".AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationId}={correlationMarker}" + + $";.AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationMarker}.{correlationId}={state}"); + if (redirect) + { + var transaction = await sendTask; + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/error?FailureMessage=" + UrlEncoder.Default.Encode("OAuth token endpoint failure: Status: BadRequest;Headers: ;Body: {\"Error\":\"Error\"};"), + transaction.Response.Headers.GetValues("Location").First()); + } + else + { + var error = await Assert.ThrowsAnyAsync(() => sendTask); + Assert.Equal("OAuth token endpoint failure: Status: BadRequest;Headers: ;Body: {\"Error\":\"Error\"};", + error.GetBaseException().Message); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReplyPathWillRejectIfAccessTokenIsMissing(bool redirect) + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("WeixinAuthTest")); + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.StateDataFormat = stateFormat; + o.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + return ReturnJsonResponse(new object()); + } + }; + o.Events = redirect ? new OAuthEvents() + { + OnRemoteFailure = ctx => + { + ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); + ctx.HandleResponse(); + return Task.FromResult(0); + } + } : new OAuthEvents(); + }); + var properties = new AuthenticationProperties(); + properties.Items.Add(correlationKey, correlationId); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var sendTask = server.SendAsync( + $"https://example.com/signin-{WeixinAuthDefaults.AuthenticationScheme}?code=TestCode&state={UrlEncoder.Default.Encode(correlationId)}", + $".AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationId}={correlationMarker}" + + $";.AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationMarker}.{correlationId}={state}"); + if (redirect) + { + var transaction = await sendTask; + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/error?FailureMessage=" + UrlEncoder.Default.Encode("Failed to retrieve access token."), + transaction.Response.Headers.GetValues("Location").First()); + } + else + { + var error = await Assert.ThrowsAnyAsync(() => sendTask); + Assert.Equal("Failed to retrieve access token.", error.GetBaseException().Message); + } + } + + [Fact] + public async Task AuthenticatedEventCanGetRefreshToken() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("WeixinAuthTest")); + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.StateDataFormat = stateFormat; + o.BackchannelHttpHandler = CreateBackchannel(); + o.Events = new OAuthEvents + { + OnCreatingTicket = context => + { + var refreshToken = context.RefreshToken; + context.Principal.AddIdentity(new ClaimsIdentity(new Claim[] { new Claim("RefreshToken", refreshToken, ClaimValueTypes.String, "WeixinAuth") }, "WeixinAuth")); + return Task.FromResult(0); + } + }; + }); + + // Skip the challenge step, go directly to the callback path + + var properties = new AuthenticationProperties(); + properties.Items.Add(correlationKey, correlationId); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + $"https://example.com/signin-{WeixinAuthDefaults.AuthenticationScheme}?code=TestCode&state={UrlEncoder.Default.Encode(correlationId)}", + $".AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationId}={correlationMarker}" + + $";.AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationMarker}.{correlationId}={state}"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.True(transaction.SetCookie.Count >= 2); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/me", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.Equal("Test Refresh Token", transaction.FindClaimValue("RefreshToken")); + } + + [Fact] + public async Task NullRedirectUriWillRedirectToSlash() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("WeixinAuthTest")); + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.StateDataFormat = stateFormat; + o.BackchannelHttpHandler = CreateBackchannel(); + o.Events = new OAuthEvents + { + OnTicketReceived = context => + { + context.Properties.RedirectUri = null; + return Task.FromResult(0); + } + }; + }); + + var properties = new AuthenticationProperties(); + properties.Items.Add(correlationKey, correlationId); + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + $"https://example.com/signin-{WeixinAuthDefaults.AuthenticationScheme}?code=TestCode&state={UrlEncoder.Default.Encode(correlationId)}", + $".AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationId}={correlationMarker}" + + $";.AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationMarker}.{correlationId}={state}"); + + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/", transaction.Response.Headers.GetValues("Location").First()); + Assert.True(transaction.SetCookie.Count >= 2); + } + + [Fact] + public async Task ValidateAuthenticatedContext() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("WeixinAuthTest")); + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.StateDataFormat = stateFormat; + //o.AccessType = "offline"; + o.Events = new OAuthEvents() + { + OnCreatingTicket = context => + { + Assert.True(context.User.ToString().Length > 0); + Assert.Equal("Test Access Token", context.AccessToken); + Assert.Equal("Test Refresh Token", context.RefreshToken); + Assert.Equal(TimeSpan.FromSeconds(3600), context.ExpiresIn); + Assert.Equal("Test Open ID", context.Identity.FindFirst(ClaimTypes.NameIdentifier)?.Value); + Assert.Equal("Test Name", context.Identity.FindFirst(ClaimTypes.Name)?.Value); + Assert.Equal("Test Open ID", context.Identity.FindFirst(WeixinAuthClaimTypes.OpenId)?.Value); + Assert.Equal("Test Union ID", context.Identity.FindFirst(WeixinAuthClaimTypes.UnionId)?.Value); + return Task.FromResult(0); + } + }; + o.BackchannelHttpHandler = CreateBackchannel(); + }); + + var properties = new AuthenticationProperties(); + properties.Items.Add(correlationKey, correlationId); + properties.RedirectUri = "/foo"; + var state = stateFormat.Protect(properties); + + //Post a message to the WeixinAuth middleware + var transaction = await server.SendAsync( + $"https://example.com/signin-{WeixinAuthDefaults.AuthenticationScheme}?code=TestCode&state={UrlEncoder.Default.Encode(correlationId)}", + $".AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationId}={correlationMarker}" + + $";.AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationMarker}.{correlationId}={state}"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/foo", transaction.Response.Headers.GetValues("Location").First()); + } + + [Fact] + public async Task NoStateCausesException() + { + var server = CreateServer(o => + { + ConfigureDefaults(o); + }); + + //Post a message to the WeixinAuth middleware + var error = await Assert.ThrowsAnyAsync(() => server.SendAsync("https://example.com/signin-WeixinAuth")); + Assert.Equal("The oauth state was missing.", error.GetBaseException().Message); + } + + [Fact] + public async Task StateDataFormatCauseException() + { + var server = CreateServer(o => + { + ConfigureDefaults(o); + }); + + //Post a message to the WeixinAuth middleware + var error = await Assert.ThrowsAnyAsync(() => server.SendAsync("https://example.com/signin-WeixinAuth?state=TestState")); + Assert.StartsWith("The oauth state cookie was missing", error.GetBaseException().Message); + } + + [Fact] + public async Task StateCorrelationMissingCauseException() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("WeixinAuthTest")); + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.StateDataFormat = stateFormat; + }); + + var properties = new AuthenticationProperties(); + properties.Items.Add(correlationKey, correlationId); + var state = stateFormat.Protect(properties); + + var error3 = await Assert.ThrowsAnyAsync(() + => server.SendAsync( + "https://example.com/signin-WeixinAuth?state=" + UrlEncoder.Default.Encode(state))); + Assert.StartsWith("The oauth state cookie was missing: ", error3.GetBaseException().Message); + } + + [Fact] + public async Task StateCorrelationMarkerWrongCauseException() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("WeixinAuthTest")); + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.StateDataFormat = stateFormat; + }); + + var properties = new AuthenticationProperties(); + properties.Items.Add(correlationKey, correlationId); + var state = stateFormat.Protect(properties); + + var error = await Assert.ThrowsAnyAsync(() + => server.SendAsync( + $"https://example.com/signin-WeixinAuth?state={UrlEncoder.Default.Encode(correlationId)}", + $".AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationId}=HERE_MUST_BE_N" + + $";.AspNetCore.Correlation.{ WeixinAuthDefaults.AuthenticationScheme}.{correlationMarker}.{correlationId}={state}")); + Assert.Equal("Correlation failed.", error.GetBaseException().Message); + } + + [Fact] + public async Task StateCorrelationSuccessCodeMissing() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("WeixinAuthTest")); + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.StateDataFormat = stateFormat; + }); + + var properties = new AuthenticationProperties(); + properties.Items.Add(correlationKey, correlationId); + var state = stateFormat.Protect(properties); + + var error2 = await Assert.ThrowsAnyAsync(() + => server.SendAsync( + "https://example.com/signin-WeixinAuth?state=" + UrlEncoder.Default.Encode(correlationId), + $".AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationId}={correlationMarker}" + + $";.AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationMarker}.{correlationId}={state}")); + Assert.Equal("Code was not found.", error2.GetBaseException().Message); + } + + [Fact] + public async Task CodeInvalidCauseException() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("WeixinAuthTest")); + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.StateDataFormat = stateFormat; + }); + + // Skip the challenge step, go directly to the callback path + + var properties = new AuthenticationProperties(); + properties.Items.Add(correlationKey, correlationId); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + + var result = await Assert.ThrowsAnyAsync(() => server.SendAsync( + $"https://example.com/signin-{WeixinAuthDefaults.AuthenticationScheme}?code=TestCode&state={UrlEncoder.Default.Encode(correlationId)}", + $".AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationId}={correlationMarker}" + + $";.AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationMarker}.{correlationId}={state}")); + + Assert.StartsWith("OAuth token endpoint failure: ", result.GetBaseException().Message); + Assert.Contains("invalid appid", result.GetBaseException().Message); + } + + [Fact] + public async Task CanRedirectOnError() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("WeixinAuthTest")); + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.Events = new OAuthEvents() + { + OnRemoteFailure = ctx => + { + ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); + ctx.HandleResponse(); + return Task.FromResult(0); + } + }; + }); + + //Post a message to the WeixinAuth middleware + var transaction = await server.SendAsync( + "https://example.com/signin-WeixinAuth?code=TestCode"); + + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/error?FailureMessage=" + UrlEncoder.Default.Encode("The oauth state was missing."), + transaction.Response.Headers.GetValues("Location").First()); + } + + [Fact] + public async Task AuthenticateAutomaticWhenAlreadySignedInSucceeds() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("WeixinAuthTest")); + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.SilentMode = false; + o.StateDataFormat = stateFormat; + o.SaveTokens = true; + o.BackchannelHttpHandler = CreateBackchannel(); + }); + + // Skip the challenge step, go directly to the callback path + + var properties = new AuthenticationProperties(); + properties.Items.Add(correlationKey, correlationId); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + $"https://example.com/signin-{WeixinAuthDefaults.AuthenticationScheme}?code=TestCode&state={UrlEncoder.Default.Encode(correlationId)}", + $".AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationId}={correlationMarker}" + + $";.AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationMarker}.{correlationId}={state}"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.True(transaction.SetCookie.Count >= 2); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/authenticate", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.Equal("Test Name", transaction.FindClaimValue(ClaimTypes.Name)); + Assert.Equal("Test Open ID", transaction.FindClaimValue(ClaimTypes.NameIdentifier)); + Assert.Equal("Test Open ID", transaction.FindClaimValue(WeixinAuthClaimTypes.OpenId)); + Assert.Equal("Test Union ID", transaction.FindClaimValue(WeixinAuthClaimTypes.UnionId)); + + // Ensure claims transformation + Assert.Equal("yup", transaction.FindClaimValue("xform")); + } + + [Fact] + public async Task AuthenticateWeixinAuthWhenAlreadySignedInSucceeds() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("WeixinAuthTest")); + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.StateDataFormat = stateFormat; + o.SaveTokens = true; + o.BackchannelHttpHandler = CreateBackchannel(); + }); + + // Skip the challenge step, go directly to the callback path + + var properties = new AuthenticationProperties(); + properties.Items.Add(correlationKey, correlationId); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + $"https://example.com/signin-{WeixinAuthDefaults.AuthenticationScheme}?code=TestCode&state={UrlEncoder.Default.Encode(correlationId)}", + $".AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationId}={correlationMarker}" + + $";.AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationMarker}.{correlationId}={state}"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.True(transaction.SetCookie.Count >= 2); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/authenticate-WeixinAuth", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.Equal("Test Name", transaction.FindClaimValue(ClaimTypes.Name)); + Assert.Equal("Test Open ID", transaction.FindClaimValue(ClaimTypes.NameIdentifier)); + Assert.Equal("Test Open ID", transaction.FindClaimValue(WeixinAuthClaimTypes.OpenId)); + Assert.Equal("Test Union ID", transaction.FindClaimValue(WeixinAuthClaimTypes.UnionId)); + + // Ensure claims transformation + Assert.Equal("yup", transaction.FindClaimValue("xform")); + } + + [Fact] + public async Task AuthenticateTwitterWhenAlreadySignedWithWeixinAuthReturnsNull() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("WeixinAuthTest")); + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.StateDataFormat = stateFormat; + o.SaveTokens = true; + o.BackchannelHttpHandler = CreateBackchannel(); + }); + + // Skip the challenge step, go directly to the callback path + + var properties = new AuthenticationProperties(); + properties.Items.Add(correlationKey, correlationId); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + $"https://example.com/signin-{WeixinAuthDefaults.AuthenticationScheme}?code=TestCode&state={UrlEncoder.Default.Encode(correlationId)}", + $".AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationId}={correlationMarker}" + + $";.AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationMarker}.{correlationId}={state}"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.True(transaction.SetCookie.Count >= 2); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/authenticate-twitter", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + Assert.Null(transaction.FindClaimValue(ClaimTypes.Name)); + } + + [Fact] + public async Task ChallengeTwitterWhenAlreadySignedWithWeixinAuthSucceeds() + { + var stateFormat = new PropertiesDataFormat(new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("WeixinAuthTest")); + var server = CreateServer(o => + { + ConfigureDefaults(o); + o.StateDataFormat = stateFormat; + o.SaveTokens = true; + o.BackchannelHttpHandler = CreateBackchannel(); + }); + + // Skip the challenge step, go directly to the callback path + + var properties = new AuthenticationProperties(); + properties.Items.Add(correlationKey, correlationId); + properties.RedirectUri = "/me"; + var state = stateFormat.Protect(properties); + var transaction = await server.SendAsync( + $"https://example.com/signin-{WeixinAuthDefaults.AuthenticationScheme}?code=TestCode&state={UrlEncoder.Default.Encode(correlationId)}", + $".AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationId}={correlationMarker}" + + $";.AspNetCore.Correlation.{WeixinAuthDefaults.AuthenticationScheme}.{correlationMarker}.{correlationId}={state}"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + Assert.True(transaction.SetCookie.Count >= 2); + + var authCookie = transaction.AuthenticationCookieValue; + //transaction = await server.SendAsync("https://example.com/challenge-twitter", authCookie); + //Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + //Assert.StartsWith("https://www.twitter.com/", transaction.Response.Headers.Location.OriginalString); + } + + private HttpMessageHandler CreateBackchannel() + { + return new TestHttpMessageHandler() + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri.StartsWith(WeixinAuthDefaults.TokenEndpoint)) + { + return ReturnJsonResponse(new + { + access_token = "Test Access Token", + expires_in = 3600, + refresh_token = "Test Refresh Token", + openid = "Test Open ID", + scope = "Test_Scope,snsapi_userinfo", + unionid = "Test Union ID" + //token_type = "Bearer" + }); + } + else if (req.RequestUri.AbsoluteUri.StartsWith(WeixinAuthDefaults.UserInformationEndpoint)) + { + return ReturnJsonResponse(new + { + openid = "Test Open ID", + nickname = "Test Name", + sex = 1, + province = "Test Province", + city = "Test City", + country = "Test Country", + headimgurl = "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0", + privilege = new[] + { + "PRIVILEGE1", + "PRIVILEGE2" + }, + unionid = "Test Union ID" + }); + } + + throw new NotImplementedException(req.RequestUri.AbsoluteUri); + } + }; + } + + private static HttpResponseMessage ReturnJsonResponse(object content, HttpStatusCode code = HttpStatusCode.OK) + { + var res = new HttpResponseMessage(code); + var text = JsonSerializer.Serialize(content); + res.Content = new StringContent(text, Encoding.UTF8, "application/json"); + return res; + } + + private class ClaimsTransformer : IClaimsTransformation + { + public Task TransformAsync(ClaimsPrincipal p) + { + 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 static TestServer CreateServer(Action configureOptions, Func testpath = null) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseAuthentication(); + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + if (req.Path == new PathString("/challenge")) + { + await context.ChallengeAsync(); + } + else if (req.Path == new PathString("/challenge-twitter")) + { + await context.ChallengeAsync(TwitterDefaults.AuthenticationScheme); + } + else if (req.Path == new PathString("/challenge-WeixinAuth")) + { + var provider = WeixinAuthDefaults.AuthenticationScheme; + string userId = "1234567890123456789012"; + // Request a redirect to the external login provider. + var redirectUrl = "/Account/ExternalLoginCallback?returnUrl=%2FHome%2FUserInfo"; + var properties = new AuthenticationProperties { RedirectUri = redirectUrl }; + properties.Items["LoginProvider"] = provider; + if (userId != null) + { + properties.Items["XsrfId"] = userId; + } + await context.ChallengeAsync(WeixinAuthDefaults.AuthenticationScheme, properties); + } + else if (req.Path == new PathString("/tokens")) + { + var result = await context.AuthenticateAsync(TestExtensions.CookieAuthenticationScheme); + var tokens = result.Properties.GetTokens(); + await res.DescribeAsync(tokens); + } + else if (req.Path == new PathString("/me")) + { + await res.DescribeAsync(context.User); + } + else if (req.Path == new PathString("/authenticate")) + { + var result = await context.AuthenticateAsync(TestExtensions.CookieAuthenticationScheme); + await res.DescribeAsync(result.Principal); + } + else if (req.Path == new PathString("/authenticate-WeixinAuth")) + { + var result = await context.AuthenticateAsync(WeixinAuthDefaults.AuthenticationScheme); + await res.DescribeAsync(result?.Principal); + } + else if (req.Path == new PathString("/authenticate-twitter")) + { + var result = await context.AuthenticateAsync(TwitterDefaults.AuthenticationScheme); + await res.DescribeAsync(result?.Principal); + } + else if (req.Path == new PathString("/401")) + { + res.StatusCode = (int)HttpStatusCode.Unauthorized;// 401; + } + else if (req.Path == new PathString("/unauthorized")) + { + // Simulate Authorization failure + var result = await context.AuthenticateAsync(WeixinAuthDefaults.AuthenticationScheme); + await context.ChallengeAsync(WeixinAuthDefaults.AuthenticationScheme); + } + else if (req.Path == new PathString("/unauthorized-auto")) + { + var result = await context.AuthenticateAsync(WeixinAuthDefaults.AuthenticationScheme); + await context.ChallengeAsync(WeixinAuthDefaults.AuthenticationScheme); + } + else if (req.Path == new PathString("/signin")) + { + await Assert.ThrowsAsync(() => context.SignInAsync(WeixinAuthDefaults.AuthenticationScheme, new ClaimsPrincipal())); + } + else if (req.Path == new PathString("/signout")) + { + await Assert.ThrowsAsync(() => context.SignOutAsync(WeixinAuthDefaults.AuthenticationScheme)); + } + else if (req.Path == new PathString("/forbid")) + { + await context.ForbidAsync(WeixinAuthDefaults.AuthenticationScheme); + } + else if (testpath != null) + { + await testpath(context); + } + else + { + await next(); + } + }); + }) + .ConfigureServices(services => + { + services.AddTransient(); + services.AddAuthentication(TestExtensions.CookieAuthenticationScheme) + .AddCookie(TestExtensions.CookieAuthenticationScheme, o => o.ForwardChallenge = WeixinAuthDefaults.AuthenticationScheme) + .AddWeixinAuth(configureOptions) + .AddTwitter(o => + { + o.ConsumerKey = "Test Twitter ClientId"; + o.ConsumerSecret = "Test Twitter AppSecret"; + o.SignInScheme = TestExtensions.CookieAuthenticationScheme; + }); + }); + return new TestServer(builder); + } + + private class TestStateDataFormat : ISecureDataFormat + { + private AuthenticationProperties Data { get; set; } + + public string Protect(AuthenticationProperties data) + { + return "protected_state"; + } + + public string Protect(AuthenticationProperties data, string purpose) + { + throw new NotImplementedException(); + } + + public AuthenticationProperties Unprotect(string protectedText) + { + Assert.Equal("protected_state", protectedText); + var properties = new AuthenticationProperties(new Dictionary() + { + { ".xsrf", "corrilationId" }, + { "testkey", "testvalue" } + }); + properties.RedirectUri = "http://testhost/redirect"; + return properties; + } + + public AuthenticationProperties Unprotect(string protectedText, string purpose) + { + throw new NotImplementedException(); + } + } + } +}