From 7d137f36c76e31033689daffb3c0c0be7852de5d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Feb 2026 04:10:27 +0000 Subject: [PATCH] Upgrade SimpleOAuth.Net from .NET Framework 3.5 to .NET 10 - Convert all projects from legacy .csproj format to SDK-style targeting net10.0 - Add HttpRequestMessage extension methods alongside existing WebRequest API - Modernize async API from APM (BeginGetResponse/EndGetResponse) to async/await - Remove Silverlight/Windows Phone conditional compilation and dead projects - Move NuGet package metadata from .nuspec into csproj - Remove duplicate UrlHelper.cs and AssemblyInfo.cs files from example projects - Add comprehensive xUnit test project with 51 tests covering: - URL encoding/decoding (RFC 3986) - Query string parsing - Token merging - Nonce/timestamp/HMAC-SHA1 generators - OAuth request signing for both WebRequest and HttpRequestMessage - Fluent API chaining - Encryption method attributes https://claude.ai/code/session_01RVbZsFLU6C1aQBGmaikCdt --- SimpleOAuth.Net-Master.sln | 82 ----- SimpleOAuth.Net.nuspec | 28 -- SimpleOAuth.Tests/EncryptionMethodTests.cs | 35 +++ SimpleOAuth.Tests/GeneratorTests.cs | 114 +++++++ SimpleOAuth.Tests/OAuthRequestWrapperTests.cs | 281 ++++++++++++++++++ SimpleOAuth.Tests/SimpleOAuth.Tests.csproj | 26 ++ SimpleOAuth.Tests/TokensTests.cs | 76 +++++ SimpleOAuth.Tests/UrlHelperTests.cs | 79 +++++ SimpleOAuth.WindowsPhone.sln | 28 -- SimpleOAuth.sln | 34 ++- SimpleOAuth/Extensions.cs | 182 ++++++------ SimpleOAuth/OAuthRequestWrapper.cs | 98 ++++-- SimpleOAuth/Properties/AssemblyInfo.cs | 36 --- SimpleOAuth/SimpleOAuth.csproj | 80 ++--- SimpleOAuthTester/Program.cs | 5 +- SimpleOAuthTester/Properties/AssemblyInfo.cs | 36 --- SimpleOAuthTester/SimpleOAuthTester.csproj | 65 +--- SimpleOAuthTester/UrlHelper.cs | 55 ---- SimpleOAuthTwitter/Program.cs | 3 - SimpleOAuthTwitter/Properties/AssemblyInfo.cs | 36 --- SimpleOAuthTwitter/SimpleOAuthTwitter.csproj | 68 +---- SimpleOAuthTwitter/UrlHelper.cs | 55 ---- SimpleOAuthTwitter/app.config | 18 -- 23 files changed, 845 insertions(+), 675 deletions(-) delete mode 100644 SimpleOAuth.Net-Master.sln delete mode 100644 SimpleOAuth.Net.nuspec create mode 100644 SimpleOAuth.Tests/EncryptionMethodTests.cs create mode 100644 SimpleOAuth.Tests/GeneratorTests.cs create mode 100644 SimpleOAuth.Tests/OAuthRequestWrapperTests.cs create mode 100644 SimpleOAuth.Tests/SimpleOAuth.Tests.csproj create mode 100644 SimpleOAuth.Tests/TokensTests.cs create mode 100644 SimpleOAuth.Tests/UrlHelperTests.cs delete mode 100644 SimpleOAuth.WindowsPhone.sln delete mode 100644 SimpleOAuth/Properties/AssemblyInfo.cs delete mode 100644 SimpleOAuthTester/Properties/AssemblyInfo.cs delete mode 100644 SimpleOAuthTester/UrlHelper.cs delete mode 100644 SimpleOAuthTwitter/Properties/AssemblyInfo.cs delete mode 100644 SimpleOAuthTwitter/UrlHelper.cs delete mode 100644 SimpleOAuthTwitter/app.config diff --git a/SimpleOAuth.Net-Master.sln b/SimpleOAuth.Net-Master.sln deleted file mode 100644 index df8f441..0000000 --- a/SimpleOAuth.Net-Master.sln +++ /dev/null @@ -1,82 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 11.00 -# Visual Studio 2010 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleOAuth", "SimpleOAuth\SimpleOAuth.csproj", "{7195A9BE-2949-4819-9BEB-7B5F366B7CBC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleOAuth.WP.Mango", "SimpleOAuth.WP.Mango\SimpleOAuth.WP.Mango.csproj", "{A0632EE3-C5C8-4ED9-9CE4-27486B2B8295}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleOAuthTester", "SimpleOAuthTester\SimpleOAuthTester.csproj", "{70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleOAuthTester.WP.Mango", "SimpleOAuthTester.WP.Mango\SimpleOAuthTester.WP.Mango.csproj", "{F22A9EAE-F610-4260-89A8-4E5856AF33C4}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleOAuthTwitter", "SimpleOAuthTwitter\SimpleOAuthTwitter.csproj", "{797DB1AA-32A5-4EEE-B5BD-73667CEE112F}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|Mixed Platforms = Debug|Mixed Platforms - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|Mixed Platforms = Release|Mixed Platforms - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Debug|x86.ActiveCfg = Debug|Any CPU - {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Release|Any CPU.Build.0 = Release|Any CPU - {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Release|x86.ActiveCfg = Release|Any CPU - {A0632EE3-C5C8-4ED9-9CE4-27486B2B8295}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A0632EE3-C5C8-4ED9-9CE4-27486B2B8295}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A0632EE3-C5C8-4ED9-9CE4-27486B2B8295}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {A0632EE3-C5C8-4ED9-9CE4-27486B2B8295}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {A0632EE3-C5C8-4ED9-9CE4-27486B2B8295}.Debug|x86.ActiveCfg = Debug|Any CPU - {A0632EE3-C5C8-4ED9-9CE4-27486B2B8295}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A0632EE3-C5C8-4ED9-9CE4-27486B2B8295}.Release|Any CPU.Build.0 = Release|Any CPU - {A0632EE3-C5C8-4ED9-9CE4-27486B2B8295}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {A0632EE3-C5C8-4ED9-9CE4-27486B2B8295}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {A0632EE3-C5C8-4ED9-9CE4-27486B2B8295}.Release|x86.ActiveCfg = Release|Any CPU - {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Debug|Any CPU.ActiveCfg = Debug|x86 - {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Debug|Mixed Platforms.Build.0 = Debug|x86 - {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Debug|x86.ActiveCfg = Debug|x86 - {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Debug|x86.Build.0 = Debug|x86 - {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Release|Any CPU.ActiveCfg = Release|x86 - {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Release|Mixed Platforms.Build.0 = Release|x86 - {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Release|x86.ActiveCfg = Release|x86 - {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Release|x86.Build.0 = Release|x86 - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Debug|Mixed Platforms.Deploy.0 = Debug|Any CPU - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Debug|x86.ActiveCfg = Debug|Any CPU - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Release|Any CPU.Build.0 = Release|Any CPU - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Release|Any CPU.Deploy.0 = Release|Any CPU - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Release|Mixed Platforms.Deploy.0 = Release|Any CPU - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Release|x86.ActiveCfg = Release|Any CPU - {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Debug|Any CPU.ActiveCfg = Debug|x86 - {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Debug|Mixed Platforms.Build.0 = Debug|x86 - {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Debug|x86.ActiveCfg = Debug|x86 - {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Debug|x86.Build.0 = Debug|x86 - {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Release|Any CPU.ActiveCfg = Release|x86 - {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Release|Mixed Platforms.Build.0 = Release|x86 - {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Release|x86.ActiveCfg = Release|x86 - {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Release|x86.Build.0 = Release|x86 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/SimpleOAuth.Net.nuspec b/SimpleOAuth.Net.nuspec deleted file mode 100644 index cb69990..0000000 --- a/SimpleOAuth.Net.nuspec +++ /dev/null @@ -1,28 +0,0 @@ - - - - SimpleOAuth.Net - 1.0.1 - Daniel McKenzie and Chris Benard - Daniel McKenzie and Chris Benard - https://github.com/cbenard/SimpleOAuth.Net/blob/master/LICENSE - https://github.com/cbenard/SimpleOAuth.Net - http://chrisbenard.net/downloads/oauthlogo.png - false - OAuth libraries come in all shapes and sizes, however in the .Net land, they only come in one - extra large. This library is made to rectify that. It's a small (~15kb) library, with no dependencies that lets you create WebRequest's and sign them to your hearts content. - Initial NuGet release - Copyright 2012 - oauth simpleoauth simpleoauth.net - - - - - - - - - - - - - \ No newline at end of file diff --git a/SimpleOAuth.Tests/EncryptionMethodTests.cs b/SimpleOAuth.Tests/EncryptionMethodTests.cs new file mode 100644 index 0000000..2eab7cc --- /dev/null +++ b/SimpleOAuth.Tests/EncryptionMethodTests.cs @@ -0,0 +1,35 @@ +using SimpleOAuth.Internal; + +namespace SimpleOAuth.Tests; + +public class EncryptionMethodTests +{ + [Fact] + public void Plain_HasCorrectDescription() + { + var description = EncryptionMethod.Plain.GetDescription(); + Assert.Equal("PLAINTEXT", description); + } + + [Fact] + public void HMACSHA1_HasCorrectDescription() + { + var description = EncryptionMethod.HMACSHA1.GetDescription(); + Assert.Equal("HMAC-SHA1", description); + } + + [Fact] + public void HMACSHA1_HasSignatureType() + { + var signatureType = EncryptionMethod.HMACSHA1.GetSignatureType(); + Assert.NotNull(signatureType); + Assert.Equal(typeof(Generators.HmacSha1Generator), signatureType); + } + + [Fact] + public void Plain_HasNoSignatureType() + { + var signatureType = EncryptionMethod.Plain.GetSignatureType(); + Assert.Null(signatureType); + } +} diff --git a/SimpleOAuth.Tests/GeneratorTests.cs b/SimpleOAuth.Tests/GeneratorTests.cs new file mode 100644 index 0000000..4b7994e --- /dev/null +++ b/SimpleOAuth.Tests/GeneratorTests.cs @@ -0,0 +1,114 @@ +using SimpleOAuth.Generators; + +namespace SimpleOAuth.Tests; + +public class NonceGeneratorTests +{ + [Fact] + public void Generate_ReturnsNonEmptyString() + { + var generator = new NonceGenerator(); + var nonce = generator.Generate(); + Assert.False(string.IsNullOrEmpty(nonce)); + } + + [Fact] + public void Generate_ReturnsUniqueValues() + { + var generator = new NonceGenerator(); + var nonces = new HashSet(); + for (int i = 0; i < 100; i++) + { + nonces.Add(generator.Generate()); + } + Assert.Equal(100, nonces.Count); + } + + [Fact] + public void Generate_ReturnsStringWithoutDashes() + { + var generator = new NonceGenerator(); + var nonce = generator.Generate(); + Assert.DoesNotContain("-", nonce); + } + + [Fact] + public void Generate_Returns32CharacterString() + { + var generator = new NonceGenerator(); + var nonce = generator.Generate(); + // GUID without dashes = 32 hex chars + Assert.Equal(32, nonce.Length); + } +} + +public class TimestampGeneratorTests +{ + [Fact] + public void Generate_ReturnsNonEmptyString() + { + var generator = new TimestampGenerator(); + var timestamp = generator.Generate(); + Assert.False(string.IsNullOrEmpty(timestamp)); + } + + [Fact] + public void Generate_ReturnsNumericString() + { + var generator = new TimestampGenerator(); + var timestamp = generator.Generate(); + Assert.True(long.TryParse(timestamp, out _)); + } + + [Fact] + public void Generate_ReturnsReasonableTimestamp() + { + var generator = new TimestampGenerator(); + var timestamp = long.Parse(generator.Generate()); + + // Should be after 2020-01-01 (1577836800) and before 2100-01-01 + Assert.True(timestamp > 1577836800); + Assert.True(timestamp < 4102444800); + } +} + +public class HmacSha1GeneratorTests +{ + [Fact] + public void Generate_ProducesConsistentOutput() + { + var generator = new HmacSha1Generator(); + var result1 = generator.Generate("base string", "key"); + var result2 = generator.Generate("base string", "key"); + Assert.Equal(result1, result2); + } + + [Fact] + public void Generate_DifferentInputsProduceDifferentOutput() + { + var generator = new HmacSha1Generator(); + var result1 = generator.Generate("base string 1", "key"); + var result2 = generator.Generate("base string 2", "key"); + Assert.NotEqual(result1, result2); + } + + [Fact] + public void Generate_DifferentKeysProduceDifferentOutput() + { + var generator = new HmacSha1Generator(); + var result1 = generator.Generate("base string", "key1"); + var result2 = generator.Generate("base string", "key2"); + Assert.NotEqual(result1, result2); + } + + [Fact] + public void Generate_ReturnsBase64String() + { + var generator = new HmacSha1Generator(); + var result = generator.Generate("test", "key"); + // Should be valid base64 + var bytes = Convert.FromBase64String(result); + // HMAC-SHA1 produces 20 bytes + Assert.Equal(20, bytes.Length); + } +} diff --git a/SimpleOAuth.Tests/OAuthRequestWrapperTests.cs b/SimpleOAuth.Tests/OAuthRequestWrapperTests.cs new file mode 100644 index 0000000..41cc244 --- /dev/null +++ b/SimpleOAuth.Tests/OAuthRequestWrapperTests.cs @@ -0,0 +1,281 @@ +using System.Net; +using System.Net.Http; + +namespace SimpleOAuth.Tests; + +public class OAuthRequestWrapperTests +{ + private readonly Tokens _testTokens = new Tokens + { + ConsumerKey = "dpf43f3p2l4k3l03", + ConsumerSecret = "kd94hf93k423kf44", + AccessToken = "nnch734d00sl2jdk", + AccessTokenSecret = "pfkkdhi9sl3r4s00" + }; + + #region WebRequest Tests + + [Fact] + public void SignRequest_WebRequest_SetsAuthorizationHeader() + { + var request = WebRequest.Create("https://example.com/resource"); + request.Method = "GET"; + + request.SignRequest(_testTokens) + .WithEncryption(EncryptionMethod.HMACSHA1) + .InHeader(); + + var authHeader = request.Headers["Authorization"]; + Assert.NotNull(authHeader); + Assert.StartsWith("OAuth ", authHeader); + } + + [Fact] + public void SignRequest_WebRequest_ContainsRequiredOAuthParameters() + { + var request = WebRequest.Create("https://example.com/resource"); + request.Method = "GET"; + + request.SignRequest(_testTokens) + .WithEncryption(EncryptionMethod.HMACSHA1) + .InHeader(); + + var authHeader = request.Headers["Authorization"]; + Assert.Contains("oauth_consumer_key", authHeader); + Assert.Contains("oauth_nonce", authHeader); + Assert.Contains("oauth_signature_method", authHeader); + Assert.Contains("oauth_timestamp", authHeader); + Assert.Contains("oauth_token", authHeader); + Assert.Contains("oauth_version", authHeader); + Assert.Contains("oauth_signature", authHeader); + } + + [Fact] + public void SignRequest_WebRequest_WithCallback_IncludesCallback() + { + var request = WebRequest.Create("https://example.com/request_token"); + request.Method = "POST"; + + request.SignRequest() + .WithTokens(_testTokens) + .WithCallback("oob") + .InHeader(); + + var authHeader = request.Headers["Authorization"]; + Assert.Contains("oauth_callback", authHeader); + } + + [Fact] + public void SignRequest_WebRequest_WithVerifier_IncludesVerifier() + { + var request = WebRequest.Create("https://example.com/access_token"); + request.Method = "POST"; + + request.SignRequest(_testTokens) + .WithVerifier("hfdp7dh39dks9884") + .InHeader(); + + var authHeader = request.Headers["Authorization"]; + Assert.Contains("oauth_verifier", authHeader); + } + + [Fact] + public void SignRequest_WebRequest_WithVersion_UsesSpecifiedVersion() + { + var request = WebRequest.Create("https://example.com/resource"); + request.Method = "GET"; + + request.SignRequest(_testTokens) + .WithVersion("1.0") + .InHeader(); + + var authHeader = request.Headers["Authorization"]; + Assert.Contains("oauth_version", authHeader); + Assert.Contains("1.0", authHeader); + } + + [Fact] + public void WithTokens_NullTokens_ThrowsArgumentException() + { + var request = WebRequest.Create("https://example.com/resource"); + var wrapper = request.SignRequest(); + + Assert.Throws(() => wrapper.WithTokens(null)); + } + + [Fact] + public void SignRequest_WebRequest_WithPostParameters_IncludesInSignature() + { + var request = WebRequest.Create("https://example.com/resource"); + request.Method = "POST"; + + request.SignRequest(_testTokens) + .WithPostParameters("status=hello") + .InHeader(); + + var authHeader = request.Headers["Authorization"]; + Assert.NotNull(authHeader); + Assert.Contains("oauth_signature", authHeader); + } + + [Fact] + public void SignRequest_WebRequest_WithPostParameters_SetsContentType() + { + var request = WebRequest.Create("https://example.com/resource"); + request.Method = "POST"; + + request.SignRequest(_testTokens) + .WithPostParameters("status=hello") + .InHeader(); + + Assert.Equal("application/x-www-form-urlencoded", request.ContentType); + } + + [Fact] + public void SignRequest_WebRequest_NoAccessToken_OmitsOAuthToken() + { + var consumerOnlyTokens = new Tokens + { + ConsumerKey = "key", + ConsumerSecret = "secret" + }; + + var request = WebRequest.Create("https://example.com/request_token"); + request.Method = "POST"; + + request.SignRequest(consumerOnlyTokens).InHeader(); + + var authHeader = request.Headers["Authorization"]; + Assert.DoesNotContain("oauth_token", authHeader); + } + + [Fact] + public void SignRequest_WebRequest_WithQueryParams_ProducesDifferentSignature() + { + var request1 = WebRequest.Create("https://example.com/resource?page=1"); + request1.Method = "GET"; + request1.SignRequest(_testTokens).InHeader(); + var header1 = request1.Headers["Authorization"]; + + var request2 = WebRequest.Create("https://example.com/resource?page=2"); + request2.Method = "GET"; + request2.SignRequest(_testTokens).InHeader(); + var header2 = request2.Headers["Authorization"]; + + // The signatures should differ because the query parameters differ + // (nonce/timestamp also differ, but the point is both produce valid headers) + Assert.NotEqual(header1, header2); + } + + #endregion + + #region HttpRequestMessage Tests + + [Fact] + public void SignRequest_HttpRequestMessage_SetsAuthorizationHeader() + { + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/resource"); + + request.SignRequest(_testTokens) + .WithEncryption(EncryptionMethod.HMACSHA1) + .InHeader(); + + Assert.True(request.Headers.Contains("Authorization")); + var authHeader = request.Headers.GetValues("Authorization").First(); + Assert.StartsWith("OAuth ", authHeader); + } + + [Fact] + public void SignRequest_HttpRequestMessage_ContainsRequiredOAuthParameters() + { + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/resource"); + + request.SignRequest(_testTokens) + .WithEncryption(EncryptionMethod.HMACSHA1) + .InHeader(); + + var authHeader = request.Headers.GetValues("Authorization").First(); + Assert.Contains("oauth_consumer_key", authHeader); + Assert.Contains("oauth_nonce", authHeader); + Assert.Contains("oauth_signature_method", authHeader); + Assert.Contains("oauth_timestamp", authHeader); + Assert.Contains("oauth_token", authHeader); + Assert.Contains("oauth_version", authHeader); + Assert.Contains("oauth_signature", authHeader); + } + + [Fact] + public void SignRequest_HttpRequestMessage_PostWithContent_SetsSignature() + { + var request = new HttpRequestMessage(HttpMethod.Post, "https://example.com/resource"); + request.Content = new FormUrlEncodedContent(new[] + { + new KeyValuePair("status", "hello") + }); + + request.SignRequest(_testTokens) + .WithPostParameters("status=hello") + .InHeader(); + + var authHeader = request.Headers.GetValues("Authorization").First(); + Assert.Contains("oauth_signature", authHeader); + } + + [Fact] + public void SignRequest_HttpRequestMessage_WithCallback_IncludesCallback() + { + var request = new HttpRequestMessage(HttpMethod.Post, "https://example.com/request_token"); + + request.SignRequest(_testTokens) + .WithCallback("https://example.com/callback") + .InHeader(); + + var authHeader = request.Headers.GetValues("Authorization").First(); + Assert.Contains("oauth_callback", authHeader); + } + + #endregion + + #region DefaultSigningMethod Tests + + [Fact] + public void DefaultSigningMethod_IsHmacSha1() + { + Assert.Equal(EncryptionMethod.HMACSHA1, OAuthRequestWrapper.DefaultSigningMethod); + } + + [Fact] + public void DefaultSigningMethod_CanBeChanged() + { + var originalDefault = OAuthRequestWrapper.DefaultSigningMethod; + try + { + OAuthRequestWrapper.DefaultSigningMethod = EncryptionMethod.Plain; + Assert.Equal(EncryptionMethod.Plain, OAuthRequestWrapper.DefaultSigningMethod); + } + finally + { + OAuthRequestWrapper.DefaultSigningMethod = originalDefault; + } + } + + #endregion + + #region Chaining API Tests + + [Fact] + public void FluentApi_ReturnsSameWrapper() + { + var request = WebRequest.Create("https://example.com/resource"); + var wrapper = request.SignRequest(); + + var result = wrapper + .WithTokens(_testTokens) + .WithEncryption(EncryptionMethod.HMACSHA1) + .WithVersion("1.0"); + + Assert.Same(wrapper, result); + } + + #endregion +} diff --git a/SimpleOAuth.Tests/SimpleOAuth.Tests.csproj b/SimpleOAuth.Tests/SimpleOAuth.Tests.csproj new file mode 100644 index 0000000..3adb1fd --- /dev/null +++ b/SimpleOAuth.Tests/SimpleOAuth.Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + false + $(NoWarn);SYSLIB0014 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SimpleOAuth.Tests/TokensTests.cs b/SimpleOAuth.Tests/TokensTests.cs new file mode 100644 index 0000000..e168ee6 --- /dev/null +++ b/SimpleOAuth.Tests/TokensTests.cs @@ -0,0 +1,76 @@ +namespace SimpleOAuth.Tests; + +public class TokensTests +{ + [Fact] + public void MergeWith_UpdatesNonEmptyTokens() + { + var original = new Tokens + { + ConsumerKey = "key1", + ConsumerSecret = "secret1", + AccessToken = "token1", + AccessTokenSecret = "tokenSecret1" + }; + + var newTokens = new Tokens + { + AccessToken = "newToken", + AccessTokenSecret = "newTokenSecret" + }; + + original.MergeWith(newTokens); + + Assert.Equal("key1", original.ConsumerKey); + Assert.Equal("secret1", original.ConsumerSecret); + Assert.Equal("newToken", original.AccessToken); + Assert.Equal("newTokenSecret", original.AccessTokenSecret); + } + + [Fact] + public void MergeWith_DoesNotOverwriteWithEmpty() + { + var original = new Tokens + { + ConsumerKey = "key1", + ConsumerSecret = "secret1", + AccessToken = "token1", + AccessTokenSecret = "tokenSecret1" + }; + + var newTokens = new Tokens + { + ConsumerKey = "", + ConsumerSecret = null, + AccessToken = "newToken", + AccessTokenSecret = "" + }; + + original.MergeWith(newTokens); + + Assert.Equal("key1", original.ConsumerKey); + Assert.Equal("secret1", original.ConsumerSecret); + Assert.Equal("newToken", original.AccessToken); + Assert.Equal("tokenSecret1", original.AccessTokenSecret); + } + + [Fact] + public void MergeWith_AllNewTokens() + { + var original = new Tokens(); + var newTokens = new Tokens + { + ConsumerKey = "key", + ConsumerSecret = "secret", + AccessToken = "token", + AccessTokenSecret = "tokenSecret" + }; + + original.MergeWith(newTokens); + + Assert.Equal("key", original.ConsumerKey); + Assert.Equal("secret", original.ConsumerSecret); + Assert.Equal("token", original.AccessToken); + Assert.Equal("tokenSecret", original.AccessTokenSecret); + } +} diff --git a/SimpleOAuth.Tests/UrlHelperTests.cs b/SimpleOAuth.Tests/UrlHelperTests.cs new file mode 100644 index 0000000..a18bec3 --- /dev/null +++ b/SimpleOAuth.Tests/UrlHelperTests.cs @@ -0,0 +1,79 @@ +using SimpleOAuth.Utilities; + +namespace SimpleOAuth.Tests; + +public class UrlHelperTests +{ + [Theory] + [InlineData("", "")] + [InlineData("hello", "hello")] + [InlineData("hello world", "hello%20world")] + [InlineData("test@example.com", "test%40example.com")] + [InlineData("100%", "100%25")] + [InlineData("a+b", "a%2Bb")] + [InlineData("foo=bar&baz=qux", "foo%3Dbar%26baz%3Dqux")] + public void Encode_EncodesCorrectly(string input, string expected) + { + var result = UrlHelper.Encode(input); + Assert.Equal(expected, result); + } + + [Fact] + public void Encode_PreservesUnreservedCharacters() + { + // RFC 3986 unreserved characters: A-Z a-z 0-9 - . _ ~ + var unreserved = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; + var result = UrlHelper.Encode(unreserved); + Assert.Equal(unreserved, result); + } + + [Fact] + public void Encode_NullReturnsEmpty() + { + var result = UrlHelper.Encode(null); + Assert.Equal(string.Empty, result); + } + + [Theory] + [InlineData("hello%20world", "hello world")] + [InlineData("hello+world", "hello world")] + [InlineData("test%40example.com", "test@example.com")] + public void Decode_DecodesCorrectly(string input, string expected) + { + var result = UrlHelper.Decode(input); + Assert.Equal(expected, result); + } + + [Fact] + public void ParseQueryString_ParsesSimpleQuery() + { + var result = UrlHelper.ParseQueryString("foo=bar&baz=qux"); + Assert.Equal(2, result.Count); + Assert.Equal("bar", result["foo"]); + Assert.Equal("qux", result["baz"]); + } + + [Fact] + public void ParseQueryString_HandlesLeadingQuestionMark() + { + var result = UrlHelper.ParseQueryString("?foo=bar&baz=qux"); + Assert.Equal(2, result.Count); + Assert.Equal("bar", result["foo"]); + Assert.Equal("qux", result["baz"]); + } + + [Fact] + public void ParseQueryString_HandlesEncodedValues() + { + var result = UrlHelper.ParseQueryString("name=hello+world&key=test%40value"); + Assert.Equal("hello world", result["name"]); + Assert.Equal("test@value", result["key"]); + } + + [Fact] + public void ParseQueryString_EmptyStringReturnsEmpty() + { + var result = UrlHelper.ParseQueryString(""); + Assert.Empty(result); + } +} diff --git a/SimpleOAuth.WindowsPhone.sln b/SimpleOAuth.WindowsPhone.sln deleted file mode 100644 index 87d0712..0000000 --- a/SimpleOAuth.WindowsPhone.sln +++ /dev/null @@ -1,28 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 11.00 -# Visual Studio 2010 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleOAuth.WP.Mango", "SimpleOAuth.WP.Mango\SimpleOAuth.WP.Mango.csproj", "{A0632EE3-C5C8-4ED9-9CE4-27486B2B8295}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleOAuthTester.WP.Mango", "SimpleOAuthTester.WP.Mango\SimpleOAuthTester.WP.Mango.csproj", "{F22A9EAE-F610-4260-89A8-4E5856AF33C4}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A0632EE3-C5C8-4ED9-9CE4-27486B2B8295}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A0632EE3-C5C8-4ED9-9CE4-27486B2B8295}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A0632EE3-C5C8-4ED9-9CE4-27486B2B8295}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A0632EE3-C5C8-4ED9-9CE4-27486B2B8295}.Release|Any CPU.Build.0 = Release|Any CPU - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Release|Any CPU.Build.0 = Release|Any CPU - {F22A9EAE-F610-4260-89A8-4E5856AF33C4}.Release|Any CPU.Deploy.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/SimpleOAuth.sln b/SimpleOAuth.sln index 2648395..c892603 100644 --- a/SimpleOAuth.sln +++ b/SimpleOAuth.sln @@ -1,5 +1,5 @@  -Microsoft Visual Studio Solution File, Format Version 11.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual C# Express 2010 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleOAuth", "SimpleOAuth\SimpleOAuth.csproj", "{7195A9BE-2949-4819-9BEB-7B5F366B7CBC}" EndProject @@ -7,14 +7,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleOAuthTwitter", "Simpl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleOAuthTester", "SimpleOAuthTester\SimpleOAuthTester.csproj", "{70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleOAuth.Tests", "SimpleOAuth.Tests\SimpleOAuth.Tests.csproj", "{AA8F952A-0E2C-45F4-A276-6C68EA81A260}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|Mixed Platforms = Debug|Mixed Platforms Debug|x86 = Debug|x86 + Debug|x64 = Debug|x64 Release|Any CPU = Release|Any CPU Release|Mixed Platforms = Release|Mixed Platforms Release|x86 = Release|x86 + Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -22,31 +26,59 @@ Global {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Debug|x86.ActiveCfg = Debug|Any CPU + {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Debug|x64.ActiveCfg = Debug|Any CPU + {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Debug|x64.Build.0 = Debug|Any CPU {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Release|Any CPU.ActiveCfg = Release|Any CPU {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Release|Any CPU.Build.0 = Release|Any CPU {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Release|Mixed Platforms.Build.0 = Release|Any CPU {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Release|x86.ActiveCfg = Release|Any CPU + {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Release|x64.ActiveCfg = Release|Any CPU + {7195A9BE-2949-4819-9BEB-7B5F366B7CBC}.Release|x64.Build.0 = Release|Any CPU {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Debug|Any CPU.ActiveCfg = Debug|x86 {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Debug|Mixed Platforms.Build.0 = Debug|x86 {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Debug|x86.ActiveCfg = Debug|x86 {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Debug|x86.Build.0 = Debug|x86 + {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Debug|x64.ActiveCfg = Debug|Any CPU + {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Debug|x64.Build.0 = Debug|Any CPU {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Release|Any CPU.ActiveCfg = Release|x86 {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Release|Mixed Platforms.ActiveCfg = Release|x86 {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Release|Mixed Platforms.Build.0 = Release|x86 {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Release|x86.ActiveCfg = Release|x86 {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Release|x86.Build.0 = Release|x86 + {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Release|x64.ActiveCfg = Release|Any CPU + {797DB1AA-32A5-4EEE-B5BD-73667CEE112F}.Release|x64.Build.0 = Release|Any CPU {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Debug|Any CPU.ActiveCfg = Debug|x86 {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Debug|Mixed Platforms.Build.0 = Debug|x86 {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Debug|x86.ActiveCfg = Debug|x86 {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Debug|x86.Build.0 = Debug|x86 + {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Debug|x64.ActiveCfg = Debug|Any CPU + {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Debug|x64.Build.0 = Debug|Any CPU {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Release|Any CPU.ActiveCfg = Release|x86 {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Release|Mixed Platforms.ActiveCfg = Release|x86 {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Release|Mixed Platforms.Build.0 = Release|x86 {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Release|x86.ActiveCfg = Release|x86 {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Release|x86.Build.0 = Release|x86 + {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Release|x64.ActiveCfg = Release|Any CPU + {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA}.Release|x64.Build.0 = Release|Any CPU + {AA8F952A-0E2C-45F4-A276-6C68EA81A260}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA8F952A-0E2C-45F4-A276-6C68EA81A260}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA8F952A-0E2C-45F4-A276-6C68EA81A260}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {AA8F952A-0E2C-45F4-A276-6C68EA81A260}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {AA8F952A-0E2C-45F4-A276-6C68EA81A260}.Debug|x86.ActiveCfg = Debug|Any CPU + {AA8F952A-0E2C-45F4-A276-6C68EA81A260}.Debug|x86.Build.0 = Debug|Any CPU + {AA8F952A-0E2C-45F4-A276-6C68EA81A260}.Debug|x64.ActiveCfg = Debug|Any CPU + {AA8F952A-0E2C-45F4-A276-6C68EA81A260}.Debug|x64.Build.0 = Debug|Any CPU + {AA8F952A-0E2C-45F4-A276-6C68EA81A260}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA8F952A-0E2C-45F4-A276-6C68EA81A260}.Release|Any CPU.Build.0 = Release|Any CPU + {AA8F952A-0E2C-45F4-A276-6C68EA81A260}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {AA8F952A-0E2C-45F4-A276-6C68EA81A260}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {AA8F952A-0E2C-45F4-A276-6C68EA81A260}.Release|x86.ActiveCfg = Release|Any CPU + {AA8F952A-0E2C-45F4-A276-6C68EA81A260}.Release|x86.Build.0 = Release|Any CPU + {AA8F952A-0E2C-45F4-A276-6C68EA81A260}.Release|x64.ActiveCfg = Release|Any CPU + {AA8F952A-0E2C-45F4-A276-6C68EA81A260}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/SimpleOAuth/Extensions.cs b/SimpleOAuth/Extensions.cs index f508be2..92916d4 100644 --- a/SimpleOAuth/Extensions.cs +++ b/SimpleOAuth/Extensions.cs @@ -1,12 +1,12 @@ -// Simple OAuth .Net +// Simple OAuth .Net // (c) 2012 Daniel McKenzie // Simple OAuth .Net may be freely distributed under the MIT license. using System; -using System.Collections.Generic; -using System.Text; -using System.Net; using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; using SimpleOAuth.Utilities; namespace SimpleOAuth @@ -16,6 +16,7 @@ namespace SimpleOAuth /// public static class Extensions { + #region WebRequest Extensions /// /// Begin signing this object with OAuth. @@ -38,7 +39,6 @@ public static OAuthRequestWrapper SignRequest(this WebRequest request, Tokens wi return new OAuthRequestWrapper(request) { RequestTokens = withTokens }; } -#if !SILVERLIGHT /// /// For the Request and Access Token stages, makes the request and parses out the OAuth tokens from /// the server. @@ -46,19 +46,6 @@ public static OAuthRequestWrapper SignRequest(this WebRequest request, Tokens wi /// The request that needs to be signed with OAuth. /// A object containing the Access Token and Access Secret provided by the OAuth server. /// You typically call this when making a request to get the users access tokens and combine this with the function. - /// - /// - /// var request = WebRequest.Create("https://api.twitter.com/oauth/request_token") { Method = "POST" }; - /// request.SignRequest(RequestTokens) - /// .WithCallback("oob") - /// .InHeader(); - /// - /// var accessTokens = request.GetOAuthTokens(); - /// RequestTokens.MergeWith(accessTokens); - /// - /// In the above example, the is created, and signed with a specific set of . A call to - /// is made and then merged with the original Request Tokens. - /// /// Thrown when the encounters an error. public static Tokens GetOAuthTokens(this WebRequest request) { @@ -79,97 +66,116 @@ public static Tokens GetOAuthTokens(this WebRequest request) newTokens.AccessToken = dataValues["oauth_token"]; newTokens.AccessTokenSecret = dataValues["oauth_token_secret"]; - } return newTokens; } -#endif /// - /// For the Request and Access Token stages, makes the request and parses out the OAuth tokens from + /// For the Request and Access Token stages, makes the request asynchronously and parses out the OAuth tokens from /// the server. /// /// The request that needs to be signed with OAuth. /// A object containing the Access Token and Access Secret provided by the OAuth server. /// You typically call this when making a request to get the users access tokens and combine this with the function. - /// - /// - /// var request = WebRequest.Create("https://api.twitter.com/oauth/request_token") { Method = "POST" }; - /// request.SignRequest(RequestTokens) - /// .WithCallback("oob") - /// .InHeader(); - /// - /// var accessTokens = request.GetOAuthTokens(); - /// RequestTokens.MergeWith(accessTokens); - /// - /// In the above example, the is created, and signed with a specific set of . A call to - /// is made and then merged with the original Request Tokens. - /// /// Thrown when the encounters an error. - public static void GetOAuthTokensAsync(this WebRequest request, Action callback) + public static async Task GetOAuthTokensAsync(this WebRequest request) { var newTokens = new Tokens(); - var output = string.Empty; - - WebResponse response = null; - StreamReader responseStreamReader = null; - Exception thrownException = null; + using (var response = await request.GetResponseAsync()) + using (var reader = new StreamReader(response.GetResponseStream())) + { + var output = await reader.ReadToEndAsync(); - request.BeginGetResponse((responseResult) => + if (!String.IsNullOrEmpty(output)) { - try + var dataValues = UrlHelper.ParseQueryString(output); + + if (!dataValues.ContainsKey("oauth_token") + || !dataValues.ContainsKey("oauth_token_secret")) { - try - { - response = request.EndGetResponse(responseResult); - - responseStreamReader = new StreamReader(response.GetResponseStream()); - - output = responseStreamReader.ReadToEnd(); - - if (!String.IsNullOrEmpty(output)) - { - var dataValues = UrlHelper.ParseQueryString(output); - - if (!dataValues.ContainsKey("oauth_token") - || !dataValues.ContainsKey("oauth_token_secret")) - { - var ex = new Exception("Response did not contain oauth_token and oauth_token_secret. Response is contained in Data of exception."); - ex.Data.Add("ResponseText", output); - ex.Data.Add("RequestUri", request.RequestUri); - ex.Data.Add("RequestMethod", request.Method); - ex.Data.Add("RequestHeaders", request.Headers); - ex.Data.Add("ResponseHeaders", response.Headers); - throw ex; - } - newTokens.AccessToken = dataValues["oauth_token"]; - newTokens.AccessTokenSecret = dataValues["oauth_token_secret"]; - } - } - catch (Exception ex) - { - thrownException = ex; - } - - if (thrownException != null) - { - callback(null, thrownException); - } - else - { - callback(newTokens, null); - } + var ex = new InvalidOperationException( + "Response did not contain oauth_token and oauth_token_secret."); + ex.Data.Add("ResponseText", output); + ex.Data.Add("RequestUri", request.RequestUri); + ex.Data.Add("RequestMethod", request.Method); + throw ex; } - finally + + newTokens.AccessToken = dataValues["oauth_token"]; + newTokens.AccessTokenSecret = dataValues["oauth_token_secret"]; + } + } + + return newTokens; + } + + #endregion + + #region HttpRequestMessage Extensions + + /// + /// Begin signing this with OAuth. + /// + /// The HTTP request message that needs to be signed with OAuth. + /// An used to provide the required parameters for OAuth signing. + public static OAuthRequestWrapper SignRequest(this HttpRequestMessage request) + { + return new OAuthRequestWrapper(request); + } + + /// + /// Begin signing this with OAuth using the tokens provided. + /// + /// The HTTP request message that needs to be signed with OAuth. + /// The to use to sign the request with. + /// An used to provide the required parameters for OAuth signing. + public static OAuthRequestWrapper SignRequest(this HttpRequestMessage request, Tokens withTokens) + { + return new OAuthRequestWrapper(request) { RequestTokens = withTokens }; + } + + /// + /// Sends the signed request using the provided and parses out the OAuth tokens + /// from the server response. + /// + /// The signed HTTP request message. + /// The to send the request with. + /// A object containing the Access Token and Access Secret provided by the OAuth server. + /// Thrown when the request encounters an error. + public static async Task GetOAuthTokensAsync(this HttpRequestMessage request, HttpClient client) + { + var newTokens = new Tokens(); + + using (var response = await client.SendAsync(request)) + { + response.EnsureSuccessStatusCode(); + var output = await response.Content.ReadAsStringAsync(); + + if (!String.IsNullOrEmpty(output)) + { + var dataValues = UrlHelper.ParseQueryString(output); + + if (!dataValues.ContainsKey("oauth_token") + || !dataValues.ContainsKey("oauth_token_secret")) { - try { if (responseStreamReader != null) responseStreamReader.Dispose(); } - catch { } - try { if (response != null) ((IDisposable)response).Dispose(); } - catch { } + var ex = new InvalidOperationException( + "Response did not contain oauth_token and oauth_token_secret."); + ex.Data.Add("ResponseText", output); + ex.Data.Add("RequestUri", request.RequestUri); + ex.Data.Add("RequestMethod", request.Method); + throw ex; } - }, null); + + newTokens.AccessToken = dataValues["oauth_token"]; + newTokens.AccessTokenSecret = dataValues["oauth_token_secret"]; + } + } + + return newTokens; } + + #endregion } } diff --git a/SimpleOAuth/OAuthRequestWrapper.cs b/SimpleOAuth/OAuthRequestWrapper.cs index 34b6aa7..d0c0441 100644 --- a/SimpleOAuth/OAuthRequestWrapper.cs +++ b/SimpleOAuth/OAuthRequestWrapper.cs @@ -1,4 +1,4 @@ -// Simple OAuth .Net +// Simple OAuth .Net // (c) 2012 Daniel McKenzie // Simple OAuth .Net may be freely distributed under the MIT license. @@ -6,16 +6,15 @@ using System.Collections.Generic; using System.Text; using System.Net; +using System.Net.Http; using SimpleOAuth.Generators; using SimpleOAuth.Utilities; using SimpleOAuth.Internal; -using System.ComponentModel; -using System.IO; namespace SimpleOAuth { /// - /// Contains a and does all the necessary work in order to sign it + /// Contains a request and does all the necessary work in order to sign it /// as a valid OAuth request before it gets sent. /// public sealed class OAuthRequestWrapper @@ -49,6 +48,7 @@ public static EncryptionMethod DefaultSigningMethod #region " Properties " private WebRequest ContainedRequest { get; set; } + private HttpRequestMessage ContainedHttpRequest { get; set; } /// /// The consumer and access keys (if required) to sign the OAuth request. @@ -63,7 +63,7 @@ public static EncryptionMethod DefaultSigningMethod public EncryptionMethod SigningMethod { get; set; } /// - /// The parameters to include in the OAuth base signature string when doing a POST request. + /// The parameters to include in the OAuth base signature string when doing a POST request. /// /// Typically, this would be provided with . public string PostParameters { get; set; } @@ -72,12 +72,12 @@ public static EncryptionMethod DefaultSigningMethod /// /// The OAuth version to use, by default it is 1.0. /// - public string OAuthVersion { - get { - return _oauthVersion; - } - set { - _oauthVersion = value; + public string OAuthVersion { + get { + return _oauthVersion; + } + set { + _oauthVersion = value; } } @@ -99,7 +99,7 @@ private Dictionary AuthorizationHeader #region " Constructor " /// - /// There is only one constructor, and the OAuthRequestWrapper can only be instantiated internally to the library. + /// Creates a wrapper around a for OAuth signing. /// internal OAuthRequestWrapper(WebRequest toContain) { @@ -107,6 +107,15 @@ internal OAuthRequestWrapper(WebRequest toContain) SigningMethod = OAuthRequestWrapper.DefaultSigningMethod; } + /// + /// Creates a wrapper around an for OAuth signing. + /// + internal OAuthRequestWrapper(HttpRequestMessage toContain) + { + ContainedHttpRequest = toContain; + SigningMethod = OAuthRequestWrapper.DefaultSigningMethod; + } + #endregion #region " Chaining Methods " @@ -177,22 +186,22 @@ public OAuthRequestWrapper WithVersion(string version) } /// - /// Provide the POST request to generate a valid OAuth signature. + /// Provide the POST parameters to generate a valid OAuth signature. /// /// The POST parameters that will be sent. /// Itself to chain. /// The OAuth standard requires POST parameters sent in application/x-www-form-urlencoded - /// requests to be included in the OAuth base string to create a valid signature. If it is not provided, - /// then the request will fail. If this is set, then the of the - /// will be set to 'application/x-www-form-urlencoded' - /// if it is not already set. + /// requests to be included in the OAuth base string to create a valid signature. public OAuthRequestWrapper WithPostParameters(string parameters) { PostParameters = parameters; - if (String.IsNullOrEmpty(ContainedRequest.ContentType)) + if (ContainedRequest != null) { - ContainedRequest.ContentType = FormUrlEncodedMimeType; + if (String.IsNullOrEmpty(ContainedRequest.ContentType)) + { + ContainedRequest.ContentType = FormUrlEncodedMimeType; + } } return this; @@ -220,7 +229,16 @@ public void InHeader() builder.AppendFormat("{0}=\"{1}\"", UrlHelper.Encode(pair.Key), UrlHelper.Encode(pair.Value)); } - ContainedRequest.Headers["Authorization"] = builder.ToString(); + var headerValue = builder.ToString(); + + if (ContainedRequest != null) + { + ContainedRequest.Headers["Authorization"] = headerValue; + } + else if (ContainedHttpRequest != null) + { + ContainedHttpRequest.Headers.TryAddWithoutValidation("Authorization", headerValue); + } } #endregion @@ -237,20 +255,46 @@ private void createAuthorization() AuthorizationHeader.Add("oauth_version", OAuthVersion); } - private void createSignature() + private string GetRequestMethod() + { + if (ContainedRequest != null) + return ContainedRequest.Method; + return ContainedHttpRequest.Method.Method; + } + + private Uri GetRequestUri() { + if (ContainedRequest != null) + return ContainedRequest.RequestUri; + return ContainedHttpRequest.RequestUri; + } + + private string GetContentType() + { + if (ContainedRequest != null) + return ContainedRequest.ContentType; + if (ContainedHttpRequest?.Content != null) + return ContainedHttpRequest.Content.Headers.ContentType?.MediaType; + return null; + } - var method = ContainedRequest.Method; - var baseUrl = String.Format("{0}://{1}{2}", ContainedRequest.RequestUri.Scheme, ContainedRequest.RequestUri.Host, ContainedRequest.RequestUri.AbsolutePath); + private void createSignature() + { + var method = GetRequestMethod(); + var requestUri = GetRequestUri(); + var baseUrl = String.Format("{0}://{1}{2}", requestUri.Scheme, requestUri.Host, requestUri.AbsolutePath); var parameters = new SortedDictionary(AuthorizationHeader); - var queryParams = UrlHelper.ParseQueryString(ContainedRequest.RequestUri.Query); + var queryParams = UrlHelper.ParseQueryString(requestUri.Query); foreach (var pair in queryParams) { parameters.Add(pair.Key, pair.Value); } - if (method.Equals("POST") && !String.IsNullOrEmpty(ContainedRequest.ContentType) && ContainedRequest.ContentType.Equals(FormUrlEncodedMimeType)) + var contentType = GetContentType(); + if (method.Equals("POST", StringComparison.OrdinalIgnoreCase) + && !String.IsNullOrEmpty(contentType) + && contentType.Equals(FormUrlEncodedMimeType, StringComparison.OrdinalIgnoreCase)) { if (!String.IsNullOrEmpty(PostParameters)) { @@ -274,7 +318,7 @@ private void createSignature() } // percent encode everything - var encodedParams = String.Format("{0}&{1}&{2}", ContainedRequest.Method.ToUpper(), + var encodedParams = String.Format("{0}&{1}&{2}", method.ToUpper(), UrlHelper.Encode(baseUrl), UrlHelper.Encode(paramString.ToString())); // key @@ -282,7 +326,7 @@ private void createSignature() UrlHelper.Encode(RequestTokens.AccessTokenSecret)); // signature time! - string signature = SignatureMethod.CreateSignature(this.SigningMethod, encodedParams.ToString(), key); + string signature = SignatureMethod.CreateSignature(this.SigningMethod, encodedParams, key); AuthorizationHeader.Add("oauth_signature", signature); } diff --git a/SimpleOAuth/Properties/AssemblyInfo.cs b/SimpleOAuth/Properties/AssemblyInfo.cs deleted file mode 100644 index abe2948..0000000 --- a/SimpleOAuth/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("SimpleOAuth")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("SimpleOAuth")] -[assembly: AssemblyCopyright("Copyright © 2012")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("4cc478ed-47cb-4c0d-878d-334abfa647a9")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SimpleOAuth/SimpleOAuth.csproj b/SimpleOAuth/SimpleOAuth.csproj index 1efcaab..373c290 100644 --- a/SimpleOAuth/SimpleOAuth.csproj +++ b/SimpleOAuth/SimpleOAuth.csproj @@ -1,67 +1,25 @@ - - + + - Debug - AnyCPU - 8.0.30703 - 2.0 - {7195A9BE-2949-4819-9BEB-7B5F366B7CBC} - Library - Properties + net10.0 SimpleOAuth SimpleOAuth - v3.5 - 512 - + true + $(NoWarn);CS1591 + + + SimpleOAuth.Net + 2.0.0 + Daniel McKenzie and Chris Benard + OAuth libraries come in all shapes and sizes, however in the .Net land, they only come in one - extra large. This library is made to rectify that. It's a small library, with no dependencies that lets you sign HTTP requests to your hearts content. + Copyright 2012-2026 + oauth simpleoauth simpleoauth.net + MIT + https://github.com/cbenard/SimpleOAuth.Net - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Release\SimpleOAuth.xml - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\SimpleOAuth.xml - - - - - - - - - - - - - - - - - - - - - + - + - - - \ No newline at end of file + + diff --git a/SimpleOAuthTester/Program.cs b/SimpleOAuthTester/Program.cs index bbfe7f8..a7d9c08 100644 --- a/SimpleOAuthTester/Program.cs +++ b/SimpleOAuthTester/Program.cs @@ -1,10 +1,7 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Net; using SimpleOAuth; -using System.Diagnostics; +using SimpleOAuth.Utilities; using System.IO; namespace SimpleOAuthTester diff --git a/SimpleOAuthTester/Properties/AssemblyInfo.cs b/SimpleOAuthTester/Properties/AssemblyInfo.cs deleted file mode 100644 index 40efc7f..0000000 --- a/SimpleOAuthTester/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("SimpleOAuthTester")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("SimpleOAuthTester")] -[assembly: AssemblyCopyright("Copyright © 2012")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("ced799eb-3e91-4b87-83ee-06cf96177868")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SimpleOAuthTester/SimpleOAuthTester.csproj b/SimpleOAuthTester/SimpleOAuthTester.csproj index 4271b5a..82580f6 100644 --- a/SimpleOAuthTester/SimpleOAuthTester.csproj +++ b/SimpleOAuthTester/SimpleOAuthTester.csproj @@ -1,64 +1,15 @@ - - + + - Debug - x86 - 8.0.30703 - 2.0 - {70CD80CC-5FED-47CD-B8A5-AC7E00276EFA} Exe - Properties + net10.0 SimpleOAuthTester SimpleOAuthTester - v4.0 - Client - 512 + $(NoWarn);SYSLIB0014 - - x86 - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - x86 - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - + - - {7195A9BE-2949-4819-9BEB-7B5F366B7CBC} - SimpleOAuth - + - - - \ No newline at end of file + + diff --git a/SimpleOAuthTester/UrlHelper.cs b/SimpleOAuthTester/UrlHelper.cs deleted file mode 100644 index d1b62df..0000000 --- a/SimpleOAuthTester/UrlHelper.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Collections.Specialized; - -namespace SimpleOAuthTester -{ - /// - /// URL encoding class. Note: use at your own risk. - /// Written by: Ian Hopkins (http://www.lucidhelix.com) - /// Date: 2008-Dec-23 - /// (Ported to C# by t3rse (http://www.t3rse.com)) - /// Source: http://stackoverflow.com/questions/14731/urlencode-through-a-console-application - /// - public class UrlHelper - { - public static string Encode(string str) - { - var charClass = String.Format("0-9a-zA-Z{0}", Regex.Escape("-_.!~*'()")); - return Regex.Replace(str, - String.Format("[^{0}]", charClass), - new MatchEvaluator(EncodeEvaluator)); - } - - public static string EncodeEvaluator(Match match) - { - return (match.Value == " ") ? "+" : String.Format("%{0:X2}", Convert.ToInt32(match.Value[0])); - } - - public static string DecodeEvaluator(Match match) - { - return Convert.ToChar(int.Parse(match.Value.Substring(1), System.Globalization.NumberStyles.HexNumber)).ToString(); - } - - public static string Decode(string str) - { - return Regex.Replace(str.Replace('+', ' '), "%[0-9a-zA-Z][0-9a-zA-Z]", new MatchEvaluator(DecodeEvaluator)); - } - - public static Dictionary ParseQueryString(string query) - { - var collection = new Dictionary(); - var queryParts = query.Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var segment in queryParts) - { - var segmentParts = segment.Split('='); - collection.Add(segmentParts[0].Trim(new char[] { '?', ' ' }), UrlHelper.Decode(segmentParts[1].Trim())); - } - - return collection; - } - } -} diff --git a/SimpleOAuthTwitter/Program.cs b/SimpleOAuthTwitter/Program.cs index 89cc5ea..133add6 100644 --- a/SimpleOAuthTwitter/Program.cs +++ b/SimpleOAuthTwitter/Program.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Net; using SimpleOAuth; using System.Diagnostics; diff --git a/SimpleOAuthTwitter/Properties/AssemblyInfo.cs b/SimpleOAuthTwitter/Properties/AssemblyInfo.cs deleted file mode 100644 index fe54bef..0000000 --- a/SimpleOAuthTwitter/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("SimpleOAuthTwitter")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("SimpleOAuthTwitter")] -[assembly: AssemblyCopyright("Copyright © 2012")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("910d5b91-8a64-44b5-8877-5a369d1873f9")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SimpleOAuthTwitter/SimpleOAuthTwitter.csproj b/SimpleOAuthTwitter/SimpleOAuthTwitter.csproj index ba9547e..3c19561 100644 --- a/SimpleOAuthTwitter/SimpleOAuthTwitter.csproj +++ b/SimpleOAuthTwitter/SimpleOAuthTwitter.csproj @@ -1,67 +1,15 @@ - - + + - Debug - x86 - 8.0.30703 - 2.0 - {797DB1AA-32A5-4EEE-B5BD-73667CEE112F} Exe - Properties + net10.0 SimpleOAuthTwitter SimpleOAuthTwitter - v4.0 - Client - 512 + $(NoWarn);SYSLIB0014 - - x86 - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - x86 - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - {7195A9BE-2949-4819-9BEB-7B5F366B7CBC} - SimpleOAuth - - + - + - - - \ No newline at end of file + + diff --git a/SimpleOAuthTwitter/UrlHelper.cs b/SimpleOAuthTwitter/UrlHelper.cs deleted file mode 100644 index 4c8761f..0000000 --- a/SimpleOAuthTwitter/UrlHelper.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Collections.Specialized; - -namespace SimpleOAuthTwitter -{ - /// - /// URL encoding class. Note: use at your own risk. - /// Written by: Ian Hopkins (http://www.lucidhelix.com) - /// Date: 2008-Dec-23 - /// (Ported to C# by t3rse (http://www.t3rse.com)) - /// Source: http://stackoverflow.com/questions/14731/urlencode-through-a-console-application - /// - public class UrlHelper - { - public static string Encode(string str) - { - var charClass = String.Format("0-9a-zA-Z{0}", Regex.Escape("-_.~")); - return Regex.Replace(str, - String.Format("[^{0}]", charClass), - new MatchEvaluator(EncodeEvaluator)); - } - - public static string EncodeEvaluator(Match match) - { - return String.Format("%{0:X2}", Convert.ToInt32(match.Value[0])); - } - - public static string DecodeEvaluator(Match match) - { - return Convert.ToChar(int.Parse(match.Value.Substring(1), System.Globalization.NumberStyles.HexNumber)).ToString(); - } - - public static string Decode(string str) - { - return Regex.Replace(str.Replace('+', ' '), "%[0-9a-zA-Z][0-9a-zA-Z]", new MatchEvaluator(DecodeEvaluator)); - } - - public static Dictionary ParseQueryString(string query) - { - var collection = new Dictionary(); - var queryParts = query.Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var segment in queryParts) - { - var segmentParts = segment.Split('='); - collection.Add(segmentParts[0].Trim(new char[] { '?', ' ' }), UrlHelper.Decode(segmentParts[1].Trim())); - } - - return collection; - } - } -} diff --git a/SimpleOAuthTwitter/app.config b/SimpleOAuthTwitter/app.config deleted file mode 100644 index 8577e93..0000000 --- a/SimpleOAuthTwitter/app.config +++ /dev/null @@ -1,18 +0,0 @@ - - - - -
- - - - - - - - - - - - - \ No newline at end of file