diff options
7 files changed, 198 insertions, 62 deletions
diff --git a/src/Mvc/shared/Mvc.Views.TestCommon/TestRazorCompiledItem.cs b/src/Mvc/shared/Mvc.Views.TestCommon/TestRazorCompiledItem.cs index 4a077fb189..df74c31709 100644 --- a/src/Mvc/shared/Mvc.Views.TestCommon/TestRazorCompiledItem.cs +++ b/src/Mvc/shared/Mvc.Views.TestCommon/TestRazorCompiledItem.cs @@ -1,9 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; -using System.Globalization; using System.Security.Cryptography; using System.Text; @@ -49,20 +46,8 @@ namespace Microsoft.AspNetCore.Razor.Hosting public static string GetChecksum(string content) { - byte[] bytes; - using (var sha = SHA1.Create()) - { - bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(content)); - } - - var result = new StringBuilder(bytes.Length); - for (var i = 0; i < bytes.Length; i++) - { - // The x2 format means lowercase hex, where each byte is a 2-character string. - result.Append(bytes[i].ToString("x2", CultureInfo.InvariantCulture)); - } - - return result.ToString(); + var bytes = SHA1.HashData(Encoding.UTF8.GetBytes(content)); + return Convert.ToHexString(bytes).ToLowerInvariant(); } } }
\ No newline at end of file diff --git a/src/Security/Authentication/Facebook/src/FacebookHandler.cs b/src/Security/Authentication/Facebook/src/FacebookHandler.cs index 1c7491329f..b544bb92db 100644 --- a/src/Security/Authentication/Facebook/src/FacebookHandler.cs +++ b/src/Security/Authentication/Facebook/src/FacebookHandler.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Generic; using System.Globalization; using System.Net.Http; using System.Security.Claims; @@ -9,7 +8,6 @@ using System.Security.Cryptography; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; @@ -60,16 +58,15 @@ namespace Microsoft.AspNetCore.Authentication.Facebook private string GenerateAppSecretProof(string accessToken) { - using (var algorithm = new HMACSHA256(Encoding.ASCII.GetBytes(Options.AppSecret))) + var key = Encoding.ASCII.GetBytes(Options.AppSecret); + var tokenBytes = Encoding.ASCII.GetBytes(accessToken); + var hash = HMACSHA256.HashData(key, tokenBytes); + var builder = new StringBuilder(); + for (int i = 0; i < hash.Length; i++) { - var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(accessToken)); - var builder = new StringBuilder(); - for (int i = 0; i < hash.Length; i++) - { - builder.Append(hash[i].ToString("x2", CultureInfo.InvariantCulture)); - } - return builder.ToString(); + builder.Append(hash[i].ToString("x2", CultureInfo.InvariantCulture)); } + return builder.ToString(); } /// <inheritdoc /> diff --git a/src/Security/Authentication/Facebook/src/FacebookOptions.cs b/src/Security/Authentication/Facebook/src/FacebookOptions.cs index 21abd7777e..8d853399b2 100644 --- a/src/Security/Authentication/Facebook/src/FacebookOptions.cs +++ b/src/Security/Authentication/Facebook/src/FacebookOptions.cs @@ -88,7 +88,7 @@ namespace Microsoft.AspNetCore.Authentication.Facebook /// <summary> /// Gets or sets if the <c>appsecret_proof</c> should be generated and sent with Facebook API calls. /// </summary> - /// <remarks>See https://developers.facebook.com/docs/graph-api/securing-requests/#appsecret_proof for more details.</remarks> + /// <remarks>See https://developers.facebook.com/docs/graph-api/security#appsecret_proof for more details.</remarks> /// <value>Defaults to <see langword="true"/>.</value> public bool SendAppSecretProof { get; set; } diff --git a/src/Security/Authentication/Twitter/src/TwitterHandler.cs b/src/Security/Authentication/Twitter/src/TwitterHandler.cs index 9d18aef74d..cade21e28a 100644 --- a/src/Security/Authentication/Twitter/src/TwitterHandler.cs +++ b/src/Security/Authentication/Twitter/src/TwitterHandler.cs @@ -1,20 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; -using System.Linq; using System.Net.Http; -using System.Net.Http.Headers; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; -using System.Threading.Tasks; -using System.Xml; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; @@ -290,7 +283,7 @@ namespace Microsoft.AspNetCore.Authentication.Twitter private async Task<AccessToken> ObtainAccessTokenAsync(RequestToken token, string verifier) { - // https://dev.twitter.com/docs/api/1/post/oauth/access_token + // https://developer.twitter.com/en/docs/authentication/api-reference/access_token Logger.ObtainAccessToken(); @@ -342,16 +335,13 @@ namespace Microsoft.AspNetCore.Authentication.Twitter private static string ComputeSignature(string consumerSecret, string? tokenSecret, string signatureData) { - using (var algorithm = new HMACSHA1()) - { - algorithm.Key = Encoding.ASCII.GetBytes( - string.Format(CultureInfo.InvariantCulture, - "{0}&{1}", - Uri.EscapeDataString(consumerSecret), - string.IsNullOrEmpty(tokenSecret) ? string.Empty : Uri.EscapeDataString(tokenSecret))); - var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(signatureData)); - return Convert.ToBase64String(hash); - } + var key = Encoding.ASCII.GetBytes( + string.Format(CultureInfo.InvariantCulture, + "{0}&{1}", + Uri.EscapeDataString(consumerSecret), + string.IsNullOrEmpty(tokenSecret) ? string.Empty : Uri.EscapeDataString(tokenSecret))); + var hash = HMACSHA1.HashData(key, Encoding.ASCII.GetBytes(signatureData)); + return Convert.ToBase64String(hash); } // https://developer.twitter.com/en/docs/apps/callback-urls diff --git a/src/Security/Authentication/test/FacebookTests.cs b/src/Security/Authentication/test/FacebookTests.cs index 638e6debed..b0b08b2424 100644 --- a/src/Security/Authentication/test/FacebookTests.cs +++ b/src/Security/Authentication/test/FacebookTests.cs @@ -369,6 +369,7 @@ namespace Microsoft.AspNetCore.Authentication.Facebook Assert.Equal(1, finalUserInfoEndpoint.Count(c => c == '?')); Assert.Contains("fields=email,timezone,picture", finalUserInfoEndpoint); Assert.Contains("&access_token=", finalUserInfoEndpoint); + Assert.Contains("&appsecret_proof=b7fb6d5a4510926b4af6fe080497827d791dc45fe6541d88ba77bdf6e8e208c6&", finalUserInfoEndpoint); } private static async Task<IHost> CreateHost(Action<IApplicationBuilder> configure, Action<IServiceCollection> configureServices, Func<HttpContext, Task<bool>> handler) diff --git a/src/Security/Authentication/test/TwitterTests.cs b/src/Security/Authentication/test/TwitterTests.cs index 175f8e72a5..198cca6641 100644 --- a/src/Security/Authentication/test/TwitterTests.cs +++ b/src/Security/Authentication/test/TwitterTests.cs @@ -1,20 +1,24 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Net.Http.Headers; 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.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Net.Http.Headers; using Xunit; namespace Microsoft.AspNetCore.Authentication.Twitter @@ -381,6 +385,158 @@ namespace Microsoft.AspNetCore.Authentication.Twitter Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode); } + [Fact] + public async Task CanSignIn() + { + var stateFormat = new SecureDataFormat<RequestToken>(new RequestTokenSerializer(), new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("TwitterTest")); + using var host = await CreateHost((options) => + { + options.ConsumerKey = "Test App Id"; + options.ConsumerSecret = "PLACEHOLDER"; + options.SaveTokens = true; + options.StateDataFormat = stateFormat; + options.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://api.twitter.com/oauth/access_token") + { + var res = new HttpResponseMessage(HttpStatusCode.OK); + var content = new Dictionary<string, string>() + { + ["oauth_token"] = "Test Access Token", + ["oauth_token_secret"] = "PLACEHOLDER", + ["user_id"] = "123456", + ["screen_name"] = "@dotnet" + }; + res.Content = new FormUrlEncodedContent(content); + return res; + } + return null; + } + }; + }); + + var token = new RequestToken() + { + Token = "TestToken", + TokenSecret = "PLACEHOLDER", + Properties = new() + }; + + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + token.Properties.Items.Add(correlationKey, correlationValue); + token.Properties.RedirectUri = "/me"; + var state = stateFormat.Protect(token); + using var server = host.GetTestServer(); + var transaction = await server.SendAsync( + "https://example.com/signin-twitter?oauth_token=TestToken&oauth_verifier=TestVerifier", + $".AspNetCore.Correlation.{correlationValue}=N;__TwitterState={UrlEncoder.Default.Encode(state)}"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/me", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + var expectedIssuer = TwitterDefaults.AuthenticationScheme; + Assert.Equal("@dotnet", transaction.FindClaimValue(ClaimTypes.Name, expectedIssuer)); + Assert.Equal("123456", transaction.FindClaimValue(ClaimTypes.NameIdentifier, expectedIssuer)); + Assert.Equal("123456", transaction.FindClaimValue("urn:twitter:userid", expectedIssuer)); + Assert.Equal("@dotnet", transaction.FindClaimValue("urn:twitter:screenname", expectedIssuer)); + + 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("PLACEHOLDER", transaction.FindTokenValue("access_token_secret")); + } + + [Fact] + public async Task CanFetchUserDetails() + { + var verifyCredentialsEndpoint = "https://api.twitter.com/1.1/account/verify_credentials.json"; + var finalVerifyCredentialsEndpoint = string.Empty; + var finalAuthorizationParameter = string.Empty; + var stateFormat = new SecureDataFormat<RequestToken>(new RequestTokenSerializer(), new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("TwitterTest")); + using var host = await CreateHost((options) => + { + options.ConsumerKey = "Test App Id"; + options.ConsumerSecret = "PLACEHOLDER"; + options.RetrieveUserDetails = true; + options.StateDataFormat = stateFormat; + options.BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://api.twitter.com/oauth/access_token") + { + var res = new HttpResponseMessage(HttpStatusCode.OK); + var content = new Dictionary<string, string>() + { + ["oauth_token"] = "Test Access Token", + ["oauth_token_secret"] = "PLACEHOLDER", + ["user_id"] = "123456", + ["screen_name"] = "@dotnet" + }; + res.Content = new FormUrlEncodedContent(content); + return res; + } + if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == + new Uri(verifyCredentialsEndpoint).GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped)) + { + finalVerifyCredentialsEndpoint = req.RequestUri.ToString(); + finalAuthorizationParameter = req.Headers.Authorization.Parameter; + var res = new HttpResponseMessage(HttpStatusCode.OK); + var graphResponse = "{ \"email\": \"Test email\" }"; + res.Content = new StringContent(graphResponse, Encoding.UTF8); + return res; + } + return null; + } + }; + }); + + var token = new RequestToken() + { + Token = "TestToken", + TokenSecret = "PLACEHOLDER", + Properties = new() + }; + + var correlationKey = ".xsrf"; + var correlationValue = "TestCorrelationId"; + token.Properties.Items.Add(correlationKey, correlationValue); + token.Properties.RedirectUri = "/me"; + var state = stateFormat.Protect(token); + using var server = host.GetTestServer(); + var transaction = await server.SendAsync( + "https://example.com/signin-twitter?oauth_token=TestToken&oauth_verifier=TestVerifier", + $".AspNetCore.Correlation.{correlationValue}=N;__TwitterState={UrlEncoder.Default.Encode(state)}"); + Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); + Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First()); + + Assert.Equal(1, finalVerifyCredentialsEndpoint.Count(c => c == '?')); + Assert.Contains("include_email=true", finalVerifyCredentialsEndpoint); + + Assert.Contains("oauth_consumer_key=", finalAuthorizationParameter); + Assert.Contains("oauth_nonce=", finalAuthorizationParameter); + Assert.Contains("oauth_signature=", finalAuthorizationParameter); + Assert.Contains("oauth_signature_method=", finalAuthorizationParameter); + Assert.Contains("oauth_timestamp=", finalAuthorizationParameter); + Assert.Contains("oauth_token=", finalAuthorizationParameter); + Assert.Contains("oauth_version=", finalAuthorizationParameter); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await server.SendAsync("https://example.com/me", authCookie); + Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); + var expectedIssuer = TwitterDefaults.AuthenticationScheme; + Assert.Equal("@dotnet", transaction.FindClaimValue(ClaimTypes.Name, expectedIssuer)); + Assert.Equal("123456", transaction.FindClaimValue(ClaimTypes.NameIdentifier, expectedIssuer)); + Assert.Equal("123456", transaction.FindClaimValue("urn:twitter:userid", expectedIssuer)); + Assert.Equal("@dotnet", transaction.FindClaimValue("urn:twitter:screenname", expectedIssuer)); + Assert.Equal("Test email", transaction.FindClaimValue(ClaimTypes.Email, expectedIssuer)); + } + private static async Task<IHost> CreateHost(Action<TwitterOptions> options, Func<HttpContext, Task<bool>> handler = null) { var host = new HostBuilder() @@ -405,6 +561,16 @@ namespace Microsoft.AspNetCore.Authentication.Twitter { await Assert.ThrowsAsync<InvalidOperationException>(() => context.ForbidAsync("Twitter")); } + else if (req.Path == new PathString("/me")) + { + await res.DescribeAsync(context.User); + } + 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 (handler == null || !await handler(context)) { await next(context); @@ -418,8 +584,8 @@ namespace Microsoft.AspNetCore.Authentication.Twitter o.SignInScheme = "External"; options(o); }; - services.AddAuthentication() - .AddCookie("External", _ => { }) + services.AddAuthentication(TestExtensions.CookieAuthenticationScheme) + .AddCookie(TestExtensions.CookieAuthenticationScheme, o => o.ForwardChallenge = TwitterDefaults.AuthenticationScheme) .AddTwitter(wrapOptions); })) .Build(); diff --git a/src/Servers/IIS/IIS/test/testassets/shared/WebSockets/HandshakeHelpers.cs b/src/Servers/IIS/IIS/test/testassets/shared/WebSockets/HandshakeHelpers.cs index 8bba71271c..c86b4493e1 100644 --- a/src/Servers/IIS/IIS/test/testassets/shared/WebSockets/HandshakeHelpers.cs +++ b/src/Servers/IIS/IIS/test/testassets/shared/WebSockets/HandshakeHelpers.cs @@ -29,13 +29,10 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests throw new ArgumentNullException(nameof(requestKey)); } - using (var algorithm = SHA1.Create()) - { - string merged = requestKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - byte[] mergedBytes = Encoding.UTF8.GetBytes(merged); - byte[] hashedBytes = algorithm.ComputeHash(mergedBytes); - return Convert.ToBase64String(hashedBytes); - } + string merged = requestKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + byte[] mergedBytes = Encoding.UTF8.GetBytes(merged); + byte[] hashedBytes = SHA1.HashData(mergedBytes); + return Convert.ToBase64String(hashedBytes); } } } |