Skip to content

Commit 6f5d122

Browse files
Use one-shot static hash methods (#36368)
* Use one-shot static hash methods Use one-shot static hash methods for SHA1 and HMACSHA*. Also a readability change to use Convert.ToHexString(). * Assert on appsecret_proof Assert that the correct secret proof is added to the request URL. * Update URL Update URL that redirects. * Fix invalid documentation link Update link that resulted in a 404. * Expand TwitterHandler coverage Add test coverage for CreateTicketAsync(), ObtainAccessTokenAsync() and RetrieveUserDetailsAsync().
1 parent e6a5af6 commit 6f5d122

File tree

7 files changed

+198
-62
lines changed

7 files changed

+198
-62
lines changed

src/Mvc/shared/Mvc.Views.TestCommon/TestRazorCompiledItem.cs

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.Collections.Generic;
6-
using System.Globalization;
74
using System.Security.Cryptography;
85
using System.Text;
96

@@ -49,20 +46,8 @@ public TestRazorCompiledItem(Type type, string kind, string identifier, object[]
4946

5047
public static string GetChecksum(string content)
5148
{
52-
byte[] bytes;
53-
using (var sha = SHA1.Create())
54-
{
55-
bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(content));
56-
}
57-
58-
var result = new StringBuilder(bytes.Length);
59-
for (var i = 0; i < bytes.Length; i++)
60-
{
61-
// The x2 format means lowercase hex, where each byte is a 2-character string.
62-
result.Append(bytes[i].ToString("x2", CultureInfo.InvariantCulture));
63-
}
64-
65-
return result.ToString();
49+
var bytes = SHA1.HashData(Encoding.UTF8.GetBytes(content));
50+
return Convert.ToHexString(bytes).ToLowerInvariant();
6651
}
6752
}
6853
}

src/Security/Authentication/Facebook/src/FacebookHandler.cs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Collections.Generic;
54
using System.Globalization;
65
using System.Net.Http;
76
using System.Security.Claims;
87
using System.Security.Cryptography;
98
using System.Text;
109
using System.Text.Encodings.Web;
1110
using System.Text.Json;
12-
using System.Threading.Tasks;
1311
using Microsoft.AspNetCore.Authentication.OAuth;
1412
using Microsoft.AspNetCore.WebUtilities;
1513
using Microsoft.Extensions.Logging;
@@ -60,16 +58,15 @@ protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIden
6058

6159
private string GenerateAppSecretProof(string accessToken)
6260
{
63-
using (var algorithm = new HMACSHA256(Encoding.ASCII.GetBytes(Options.AppSecret)))
61+
var key = Encoding.ASCII.GetBytes(Options.AppSecret);
62+
var tokenBytes = Encoding.ASCII.GetBytes(accessToken);
63+
var hash = HMACSHA256.HashData(key, tokenBytes);
64+
var builder = new StringBuilder();
65+
for (int i = 0; i < hash.Length; i++)
6466
{
65-
var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(accessToken));
66-
var builder = new StringBuilder();
67-
for (int i = 0; i < hash.Length; i++)
68-
{
69-
builder.Append(hash[i].ToString("x2", CultureInfo.InvariantCulture));
70-
}
71-
return builder.ToString();
67+
builder.Append(hash[i].ToString("x2", CultureInfo.InvariantCulture));
7268
}
69+
return builder.ToString();
7370
}
7471

7572
/// <inheritdoc />

src/Security/Authentication/Facebook/src/FacebookOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public string AppSecret
8888
/// <summary>
8989
/// Gets or sets if the <c>appsecret_proof</c> should be generated and sent with Facebook API calls.
9090
/// </summary>
91-
/// <remarks>See https://developers.facebook.com/docs/graph-api/securing-requests/#appsecret_proof for more details.</remarks>
91+
/// <remarks>See https://developers.facebook.com/docs/graph-api/security#appsecret_proof for more details.</remarks>
9292
/// <value>Defaults to <see langword="true"/>.</value>
9393
public bool SendAppSecretProof { get; set; }
9494

src/Security/Authentication/Twitter/src/TwitterHandler.cs

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.Collections.Generic;
6-
using System.Diagnostics;
74
using System.Globalization;
8-
using System.Linq;
95
using System.Net.Http;
10-
using System.Net.Http.Headers;
116
using System.Security.Claims;
127
using System.Security.Cryptography;
138
using System.Text;
149
using System.Text.Encodings.Web;
1510
using System.Text.Json;
16-
using System.Threading.Tasks;
17-
using System.Xml;
1811
using Microsoft.AspNetCore.Http;
1912
using Microsoft.AspNetCore.WebUtilities;
2013
using Microsoft.Extensions.Logging;
@@ -290,7 +283,7 @@ private async Task<RequestToken> ObtainRequestTokenAsync(string callBackUri, Aut
290283

291284
private async Task<AccessToken> ObtainAccessTokenAsync(RequestToken token, string verifier)
292285
{
293-
// https://dev.twitter.com/docs/api/1/post/oauth/access_token
286+
// https://developer.twitter.com/en/docs/authentication/api-reference/access_token
294287

295288
Logger.ObtainAccessToken();
296289

@@ -342,16 +335,13 @@ private string GenerateTimeStamp()
342335

343336
private static string ComputeSignature(string consumerSecret, string? tokenSecret, string signatureData)
344337
{
345-
using (var algorithm = new HMACSHA1())
346-
{
347-
algorithm.Key = Encoding.ASCII.GetBytes(
348-
string.Format(CultureInfo.InvariantCulture,
349-
"{0}&{1}",
350-
Uri.EscapeDataString(consumerSecret),
351-
string.IsNullOrEmpty(tokenSecret) ? string.Empty : Uri.EscapeDataString(tokenSecret)));
352-
var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(signatureData));
353-
return Convert.ToBase64String(hash);
354-
}
338+
var key = Encoding.ASCII.GetBytes(
339+
string.Format(CultureInfo.InvariantCulture,
340+
"{0}&{1}",
341+
Uri.EscapeDataString(consumerSecret),
342+
string.IsNullOrEmpty(tokenSecret) ? string.Empty : Uri.EscapeDataString(tokenSecret)));
343+
var hash = HMACSHA1.HashData(key, Encoding.ASCII.GetBytes(signatureData));
344+
return Convert.ToBase64String(hash);
355345
}
356346

357347
// https://developer.twitter.com/en/docs/apps/callback-urls

src/Security/Authentication/test/FacebookTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ public async Task CustomUserInfoEndpointHasValidGraphQuery()
369369
Assert.Equal(1, finalUserInfoEndpoint.Count(c => c == '?'));
370370
Assert.Contains("fields=email,timezone,picture", finalUserInfoEndpoint);
371371
Assert.Contains("&access_token=", finalUserInfoEndpoint);
372+
Assert.Contains("&appsecret_proof=b7fb6d5a4510926b4af6fe080497827d791dc45fe6541d88ba77bdf6e8e208c6&", finalUserInfoEndpoint);
372373
}
373374

374375
private static async Task<IHost> CreateHost(Action<IApplicationBuilder> configure, Action<IServiceCollection> configureServices, Func<HttpContext, Task<bool>> handler)

src/Security/Authentication/test/TwitterTests.cs

Lines changed: 175 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using Microsoft.AspNetCore.Builder;
5-
using Microsoft.AspNetCore.Hosting;
6-
using Microsoft.AspNetCore.Http;
7-
using Microsoft.AspNetCore.TestHost;
8-
using Microsoft.Extensions.DependencyInjection;
9-
using Microsoft.Extensions.Hosting;
10-
using Microsoft.Net.Http.Headers;
114
using System;
5+
using System.Collections.Generic;
126
using System.Linq;
137
using System.Net;
148
using System.Net.Http;
159
using System.Security.Claims;
1610
using System.Text;
11+
using System.Text.Encodings.Web;
1712
using System.Threading.Tasks;
13+
using Microsoft.AspNetCore.Builder;
14+
using Microsoft.AspNetCore.DataProtection;
15+
using Microsoft.AspNetCore.Hosting;
16+
using Microsoft.AspNetCore.Http;
17+
using Microsoft.AspNetCore.TestHost;
18+
using Microsoft.Extensions.DependencyInjection;
19+
using Microsoft.Extensions.Hosting;
20+
using Microsoft.Extensions.Logging.Abstractions;
21+
using Microsoft.Net.Http.Headers;
1822
using Xunit;
1923

2024
namespace Microsoft.AspNetCore.Authentication.Twitter
@@ -381,6 +385,158 @@ public async Task BadCallbackCallsRemoteAuthFailedWithState()
381385
Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode);
382386
}
383387

388+
[Fact]
389+
public async Task CanSignIn()
390+
{
391+
var stateFormat = new SecureDataFormat<RequestToken>(new RequestTokenSerializer(), new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("TwitterTest"));
392+
using var host = await CreateHost((options) =>
393+
{
394+
options.ConsumerKey = "Test App Id";
395+
options.ConsumerSecret = "PLACEHOLDER";
396+
options.SaveTokens = true;
397+
options.StateDataFormat = stateFormat;
398+
options.BackchannelHttpHandler = new TestHttpMessageHandler
399+
{
400+
Sender = req =>
401+
{
402+
if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://api.twitter.com/oauth/access_token")
403+
{
404+
var res = new HttpResponseMessage(HttpStatusCode.OK);
405+
var content = new Dictionary<string, string>()
406+
{
407+
["oauth_token"] = "Test Access Token",
408+
["oauth_token_secret"] = "PLACEHOLDER",
409+
["user_id"] = "123456",
410+
["screen_name"] = "@dotnet"
411+
};
412+
res.Content = new FormUrlEncodedContent(content);
413+
return res;
414+
}
415+
return null;
416+
}
417+
};
418+
});
419+
420+
var token = new RequestToken()
421+
{
422+
Token = "TestToken",
423+
TokenSecret = "PLACEHOLDER",
424+
Properties = new()
425+
};
426+
427+
var correlationKey = ".xsrf";
428+
var correlationValue = "TestCorrelationId";
429+
token.Properties.Items.Add(correlationKey, correlationValue);
430+
token.Properties.RedirectUri = "/me";
431+
var state = stateFormat.Protect(token);
432+
using var server = host.GetTestServer();
433+
var transaction = await server.SendAsync(
434+
"https://example.com/signin-twitter?oauth_token=TestToken&oauth_verifier=TestVerifier",
435+
$".AspNetCore.Correlation.{correlationValue}=N;__TwitterState={UrlEncoder.Default.Encode(state)}");
436+
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
437+
Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
438+
439+
var authCookie = transaction.AuthenticationCookieValue;
440+
transaction = await server.SendAsync("https://example.com/me", authCookie);
441+
Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
442+
var expectedIssuer = TwitterDefaults.AuthenticationScheme;
443+
Assert.Equal("@dotnet", transaction.FindClaimValue(ClaimTypes.Name, expectedIssuer));
444+
Assert.Equal("123456", transaction.FindClaimValue(ClaimTypes.NameIdentifier, expectedIssuer));
445+
Assert.Equal("123456", transaction.FindClaimValue("urn:twitter:userid", expectedIssuer));
446+
Assert.Equal("@dotnet", transaction.FindClaimValue("urn:twitter:screenname", expectedIssuer));
447+
448+
transaction = await server.SendAsync("https://example.com/tokens", authCookie);
449+
Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
450+
Assert.Equal("Test Access Token", transaction.FindTokenValue("access_token"));
451+
Assert.Equal("PLACEHOLDER", transaction.FindTokenValue("access_token_secret"));
452+
}
453+
454+
[Fact]
455+
public async Task CanFetchUserDetails()
456+
{
457+
var verifyCredentialsEndpoint = "https://api.twitter.com/1.1/account/verify_credentials.json";
458+
var finalVerifyCredentialsEndpoint = string.Empty;
459+
var finalAuthorizationParameter = string.Empty;
460+
var stateFormat = new SecureDataFormat<RequestToken>(new RequestTokenSerializer(), new EphemeralDataProtectionProvider(NullLoggerFactory.Instance).CreateProtector("TwitterTest"));
461+
using var host = await CreateHost((options) =>
462+
{
463+
options.ConsumerKey = "Test App Id";
464+
options.ConsumerSecret = "PLACEHOLDER";
465+
options.RetrieveUserDetails = true;
466+
options.StateDataFormat = stateFormat;
467+
options.BackchannelHttpHandler = new TestHttpMessageHandler
468+
{
469+
Sender = req =>
470+
{
471+
if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) == "https://api.twitter.com/oauth/access_token")
472+
{
473+
var res = new HttpResponseMessage(HttpStatusCode.OK);
474+
var content = new Dictionary<string, string>()
475+
{
476+
["oauth_token"] = "Test Access Token",
477+
["oauth_token_secret"] = "PLACEHOLDER",
478+
["user_id"] = "123456",
479+
["screen_name"] = "@dotnet"
480+
};
481+
res.Content = new FormUrlEncodedContent(content);
482+
return res;
483+
}
484+
if (req.RequestUri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) ==
485+
new Uri(verifyCredentialsEndpoint).GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped))
486+
{
487+
finalVerifyCredentialsEndpoint = req.RequestUri.ToString();
488+
finalAuthorizationParameter = req.Headers.Authorization.Parameter;
489+
var res = new HttpResponseMessage(HttpStatusCode.OK);
490+
var graphResponse = "{ \"email\": \"Test email\" }";
491+
res.Content = new StringContent(graphResponse, Encoding.UTF8);
492+
return res;
493+
}
494+
return null;
495+
}
496+
};
497+
});
498+
499+
var token = new RequestToken()
500+
{
501+
Token = "TestToken",
502+
TokenSecret = "PLACEHOLDER",
503+
Properties = new()
504+
};
505+
506+
var correlationKey = ".xsrf";
507+
var correlationValue = "TestCorrelationId";
508+
token.Properties.Items.Add(correlationKey, correlationValue);
509+
token.Properties.RedirectUri = "/me";
510+
var state = stateFormat.Protect(token);
511+
using var server = host.GetTestServer();
512+
var transaction = await server.SendAsync(
513+
"https://example.com/signin-twitter?oauth_token=TestToken&oauth_verifier=TestVerifier",
514+
$".AspNetCore.Correlation.{correlationValue}=N;__TwitterState={UrlEncoder.Default.Encode(state)}");
515+
Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode);
516+
Assert.Equal("/me", transaction.Response.Headers.GetValues("Location").First());
517+
518+
Assert.Equal(1, finalVerifyCredentialsEndpoint.Count(c => c == '?'));
519+
Assert.Contains("include_email=true", finalVerifyCredentialsEndpoint);
520+
521+
Assert.Contains("oauth_consumer_key=", finalAuthorizationParameter);
522+
Assert.Contains("oauth_nonce=", finalAuthorizationParameter);
523+
Assert.Contains("oauth_signature=", finalAuthorizationParameter);
524+
Assert.Contains("oauth_signature_method=", finalAuthorizationParameter);
525+
Assert.Contains("oauth_timestamp=", finalAuthorizationParameter);
526+
Assert.Contains("oauth_token=", finalAuthorizationParameter);
527+
Assert.Contains("oauth_version=", finalAuthorizationParameter);
528+
529+
var authCookie = transaction.AuthenticationCookieValue;
530+
transaction = await server.SendAsync("https://example.com/me", authCookie);
531+
Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode);
532+
var expectedIssuer = TwitterDefaults.AuthenticationScheme;
533+
Assert.Equal("@dotnet", transaction.FindClaimValue(ClaimTypes.Name, expectedIssuer));
534+
Assert.Equal("123456", transaction.FindClaimValue(ClaimTypes.NameIdentifier, expectedIssuer));
535+
Assert.Equal("123456", transaction.FindClaimValue("urn:twitter:userid", expectedIssuer));
536+
Assert.Equal("@dotnet", transaction.FindClaimValue("urn:twitter:screenname", expectedIssuer));
537+
Assert.Equal("Test email", transaction.FindClaimValue(ClaimTypes.Email, expectedIssuer));
538+
}
539+
384540
private static async Task<IHost> CreateHost(Action<TwitterOptions> options, Func<HttpContext, Task<bool>> handler = null)
385541
{
386542
var host = new HostBuilder()
@@ -405,6 +561,16 @@ private static async Task<IHost> CreateHost(Action<TwitterOptions> options, Func
405561
{
406562
await Assert.ThrowsAsync<InvalidOperationException>(() => context.ForbidAsync("Twitter"));
407563
}
564+
else if (req.Path == new PathString("/me"))
565+
{
566+
await res.DescribeAsync(context.User);
567+
}
568+
else if (req.Path == new PathString("/tokens"))
569+
{
570+
var result = await context.AuthenticateAsync(TestExtensions.CookieAuthenticationScheme);
571+
var tokens = result.Properties.GetTokens();
572+
await res.DescribeAsync(tokens);
573+
}
408574
else if (handler == null || !await handler(context))
409575
{
410576
await next(context);
@@ -418,8 +584,8 @@ private static async Task<IHost> CreateHost(Action<TwitterOptions> options, Func
418584
o.SignInScheme = "External";
419585
options(o);
420586
};
421-
services.AddAuthentication()
422-
.AddCookie("External", _ => { })
587+
services.AddAuthentication(TestExtensions.CookieAuthenticationScheme)
588+
.AddCookie(TestExtensions.CookieAuthenticationScheme, o => o.ForwardChallenge = TwitterDefaults.AuthenticationScheme)
423589
.AddTwitter(wrapOptions);
424590
}))
425591
.Build();

src/Servers/IIS/IIS/test/testassets/shared/WebSockets/HandshakeHelpers.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,10 @@ public static string CreateResponseKey(string requestKey)
2929
throw new ArgumentNullException(nameof(requestKey));
3030
}
3131

32-
using (var algorithm = SHA1.Create())
33-
{
34-
string merged = requestKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
35-
byte[] mergedBytes = Encoding.UTF8.GetBytes(merged);
36-
byte[] hashedBytes = algorithm.ComputeHash(mergedBytes);
37-
return Convert.ToBase64String(hashedBytes);
38-
}
32+
string merged = requestKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
33+
byte[] mergedBytes = Encoding.UTF8.GetBytes(merged);
34+
byte[] hashedBytes = SHA1.HashData(mergedBytes);
35+
return Convert.ToBase64String(hashedBytes);
3936
}
4037
}
4138
}

0 commit comments

Comments
 (0)