diff options
12 files changed, 202 insertions, 18 deletions
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/GoogleConsumer.cs b/samples/DotNetOpenAuth.ApplicationBlock/GoogleConsumer.cs index 4bcca86..40cb36d 100644 --- a/samples/DotNetOpenAuth.ApplicationBlock/GoogleConsumer.cs +++ b/samples/DotNetOpenAuth.ApplicationBlock/GoogleConsumer.cs @@ -38,9 +38,21 @@ namespace DotNetOpenAuth.ApplicationBlock { /// A mapping between Google's applications and their URI scope values. /// </summary> private static readonly Dictionary<Applications, string> DataScopeUris = new Dictionary<Applications, string> { - { Applications.Contacts, "http://www.google.com/m8/feeds/" }, - { Applications.Calendar, "http://www.google.com/calendar/feeds/" }, + { Applications.Analytics, "https://www.google.com/analytics/feeds/" }, + { Applications.GoogleBase, "http://www.google.com/base/feeds/" }, { Applications.Blogger, "http://www.blogger.com/feeds" }, + { Applications.BookSearch, "http://www.google.com/books/feeds/" }, + { Applications.Calendar, "http://www.google.com/calendar/feeds/" }, + { Applications.Contacts, "http://www.google.com/m8/feeds/" }, + { Applications.DocumentsList, "http://docs.google.com/feeds/" }, + { Applications.Finance, "http://finance.google.com/finance/feeds/" }, + { Applications.Gmail, "https://mail.google.com/mail/feed/atom" }, + { Applications.Health, "https://www.google.com/h9/feeds/" }, + { Applications.OpenSocial, "http://sandbox.gmodules.com/api/" }, + { Applications.PicasaWeb, "http://picasaweb.google.com/data/" }, + { Applications.Spreadsheets, "http://spreadsheets.google.com/feeds/" }, + { Applications.WebmasterTools, "http://www.google.com/webmasters/tools/feeds/" }, + { Applications.YouTube, "http://gdata.youtube.com" }, }; /// <summary> @@ -67,6 +79,66 @@ namespace DotNetOpenAuth.ApplicationBlock { /// Blog post authoring. /// </summary> Blogger = 0x4, + + /// <summary> + /// Google Finance + /// </summary> + Finance = 0x8, + + /// <summary> + /// Gmail + /// </summary> + Gmail = 0x10, + + /// <summary> + /// Google Health + /// </summary> + Health = 0x20, + + /// <summary> + /// OpenSocial + /// </summary> + OpenSocial = 0x40, + + /// <summary> + /// Picasa Web + /// </summary> + PicasaWeb = 0x80, + + /// <summary> + /// Google Spreadsheets + /// </summary> + Spreadsheets = 0x100, + + /// <summary> + /// Webmaster Tools + /// </summary> + WebmasterTools = 0x200, + + /// <summary> + /// YouTube + /// </summary> + YouTube = 0x400, + + /// <summary> + /// Google Docs + /// </summary> + DocumentsList = 0x800, + + /// <summary> + /// Google Book Search + /// </summary> + BookSearch = 0x1000, + + /// <summary> + /// Google Base + /// </summary> + GoogleBase = 0x2000, + + /// <summary> + /// Analytics + /// </summary> + Analytics = 0x4000, } /// <summary> diff --git a/samples/OAuthServiceProvider/App_Code/DatabaseTokenManager.cs b/samples/OAuthServiceProvider/App_Code/DatabaseTokenManager.cs index d922901..275a7c9 100644 --- a/samples/OAuthServiceProvider/App_Code/DatabaseTokenManager.cs +++ b/samples/OAuthServiceProvider/App_Code/DatabaseTokenManager.cs @@ -119,4 +119,18 @@ public class DatabaseTokenManager : IServiceProviderTokenManager { tokenRow.State = TokenAuthorizationState.AuthorizedRequestToken; tokenRow.User = user; } + + public OAuthConsumer GetConsumerForToken(string token) { + if (String.IsNullOrEmpty(token)) { + throw new ArgumentNullException("requestToken"); + } + + var tokenRow = Global.DataContext.OAuthTokens.SingleOrDefault( + tokenCandidate => tokenCandidate.Token == token); + if (tokenRow == null) { + throw new ArgumentException(); + } + + return tokenRow.OAuthConsumer; + } } diff --git a/samples/OAuthServiceProvider/Members/Authorize.aspx b/samples/OAuthServiceProvider/Members/Authorize.aspx index 0fd272c..69f9498 100644 --- a/samples/OAuthServiceProvider/Members/Authorize.aspx +++ b/samples/OAuthServiceProvider/Members/Authorize.aspx @@ -7,6 +7,7 @@ <div style="background-color: Yellow"> <b>Warning</b>: Never give your login credentials to another web site or application. </div> + <asp:HiddenField runat="server" ID="OAuthAuthorizationSecToken" EnableViewState="false" /> <p>The client web site or application <asp:Label ID="consumerLabel" Font-Bold="true" runat="server" Text="[consumer]" /> wants access to your diff --git a/samples/OAuthServiceProvider/Members/Authorize.aspx.cs b/samples/OAuthServiceProvider/Members/Authorize.aspx.cs index 76eec26..68dba5a 100644 --- a/samples/OAuthServiceProvider/Members/Authorize.aspx.cs +++ b/samples/OAuthServiceProvider/Members/Authorize.aspx.cs @@ -7,11 +7,19 @@ using System.Web.UI.WebControls; using DotNetOpenAuth; using DotNetOpenAuth.OAuth; using DotNetOpenAuth.OAuth.Messages; +using System.Security.Cryptography; /// <summary> /// Conducts the user through a Consumer authorization process. /// </summary> public partial class Authorize : System.Web.UI.Page { + private static readonly RandomNumberGenerator CryptoRandomDataGenerator = new RNGCryptoServiceProvider(); + + private string AuthorizationSecret { + get { return Session["OAuthAuthorizationSecret"] as string; } + set { Session["OAuthAuthorizationSecret"] = value; } + } + protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { if (Global.PendingOAuthAuthorization == null) { @@ -20,11 +28,24 @@ public partial class Authorize : System.Web.UI.Page { ITokenContainingMessage pendingToken = Global.PendingOAuthAuthorization; var token = Global.DataContext.OAuthTokens.Single(t => t.Token == pendingToken.Token); desiredAccessLabel.Text = token.Scope; + consumerLabel.Text = Global.TokenManager.GetConsumerForToken(token.Token).ConsumerKey; + + // Generate an unpredictable secret that goes to the user agent and must come back + // with authorization to guarantee the user interacted with this page rather than + // being scripted by an evil Consumer. + byte[] randomData = new byte[8]; + CryptoRandomDataGenerator.GetBytes(randomData); + this.AuthorizationSecret = Convert.ToBase64String(randomData); + OAuthAuthorizationSecToken.Value = this.AuthorizationSecret; } } } protected void allowAccessButton_Click(object sender, EventArgs e) { + if (this.AuthorizationSecret != OAuthAuthorizationSecToken.Value) { + throw new ArgumentException(); // probably someone trying to hack in. + } + this.AuthorizationSecret = null; // clear one time use secret var pending = Global.PendingOAuthAuthorization; Global.AuthorizePendingRequestToken(); multiView.ActiveViewIndex = 1; diff --git a/src/DotNetOpenAuth.Test/OAuth/ChannelElements/HmacSha1SigningBindingElementTests.cs b/src/DotNetOpenAuth.Test/OAuth/ChannelElements/HmacSha1SigningBindingElementTests.cs index 2596bc5..fcdb5e8 100644 --- a/src/DotNetOpenAuth.Test/OAuth/ChannelElements/HmacSha1SigningBindingElementTests.cs +++ b/src/DotNetOpenAuth.Test/OAuth/ChannelElements/HmacSha1SigningBindingElementTests.cs @@ -14,7 +14,7 @@ namespace DotNetOpenAuth.Test.ChannelElements { public class HmacSha1SigningBindingElementTests : MessagingTestBase { [TestMethod] public void SignatureTest() { - UnauthorizedTokenRequest message = SigningBindingElementBaseTests.CreateTestRequestTokenMessage(this.MessageDescriptions); + UnauthorizedTokenRequest message = SigningBindingElementBaseTests.CreateTestRequestTokenMessage(this.MessageDescriptions, null); HmacSha1SigningBindingElement_Accessor hmac = new HmacSha1SigningBindingElement_Accessor(); hmac.Channel = new TestChannel(this.MessageDescriptions); diff --git a/src/DotNetOpenAuth.Test/OAuth/ChannelElements/SigningBindingElementBaseTests.cs b/src/DotNetOpenAuth.Test/OAuth/ChannelElements/SigningBindingElementBaseTests.cs index e890b6f..cff46af 100644 --- a/src/DotNetOpenAuth.Test/OAuth/ChannelElements/SigningBindingElementBaseTests.cs +++ b/src/DotNetOpenAuth.Test/OAuth/ChannelElements/SigningBindingElementBaseTests.cs @@ -15,15 +15,45 @@ namespace DotNetOpenAuth.Test.ChannelElements { public class SigningBindingElementBaseTests : MessagingTestBase { [TestMethod] public void BaseSignatureStringTest() { - UnauthorizedTokenRequest message = CreateTestRequestTokenMessage(this.MessageDescriptions); + // Tests a message sent by HTTP GET, with no query string included in the endpoint. + UnauthorizedTokenRequest message = CreateTestRequestTokenMessage( + this.MessageDescriptions, + new MessageReceivingEndpoint("https://www.google.com/accounts/OAuthGetRequestToken", HttpDeliveryMethods.AuthorizationHeaderRequest | HttpDeliveryMethods.GetRequest)); + Assert.AreEqual( + "GET&https%3A%2F%2Fwww.google.com%2Faccounts%2FOAuthGetRequestToken&oauth_consumer_key%3Dnerdbank.org%26oauth_nonce%3Dfe4045a3f0efdd1e019fa8f8ae3f5c38%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1222665749%26oauth_version%3D1.0%26scope%3Dhttp%253A%252F%252Fwww.google.com%252Fm8%252Ffeeds%252F", + SigningBindingElementBase_Accessor.ConstructSignatureBaseString(message, MessageDictionary_Accessor.AttachShadow(this.MessageDescriptions.GetAccessor(message)))); + + // Test HTTP GET with an attached query string. We're elevating the scope parameter to the query string + // and removing it from the extradata dictionary. This should NOT affect the base signature string. + message = CreateTestRequestTokenMessage( + this.MessageDescriptions, + new MessageReceivingEndpoint("https://www.google.com/accounts/OAuthGetRequestToken?scope=http://www.google.com/m8/feeds/", HttpDeliveryMethods.AuthorizationHeaderRequest | HttpDeliveryMethods.GetRequest)); + message.ExtraData.Remove("scope"); // remove it from ExtraData since we put it in the URL + Assert.AreEqual( + "GET&https%3A%2F%2Fwww.google.com%2Faccounts%2FOAuthGetRequestToken&oauth_consumer_key%3Dnerdbank.org%26oauth_nonce%3Dfe4045a3f0efdd1e019fa8f8ae3f5c38%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1222665749%26oauth_version%3D1.0%26scope%3Dhttp%253A%252F%252Fwww.google.com%252Fm8%252Ffeeds%252F", + SigningBindingElementBase_Accessor.ConstructSignatureBaseString(message, MessageDictionary_Accessor.AttachShadow(this.MessageDescriptions.GetAccessor(message)))); + + // Test HTTP POST, with query string as well + message = CreateTestRequestTokenMessage( + this.MessageDescriptions, + new MessageReceivingEndpoint("https://www.google.com/accounts/OAuthGetRequestToken?scope=http://www.google.com/m8/feeds/", HttpDeliveryMethods.AuthorizationHeaderRequest | HttpDeliveryMethods.PostRequest)); + message.ExtraData.Remove("scope"); // remove it from ExtraData since we put it in the URL + Assert.AreEqual( + "GET&https%3A%2F%2Fwww.google.com%2Faccounts%2FOAuthGetRequestToken&oauth_consumer_key%3Dnerdbank.org%26oauth_nonce%3Dfe4045a3f0efdd1e019fa8f8ae3f5c38%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1222665749%26oauth_version%3D1.0%26scope%3Dhttp%253A%252F%252Fwww.google.com%252Fm8%252Ffeeds%252F", + SigningBindingElementBase_Accessor.ConstructSignatureBaseString(message, MessageDictionary_Accessor.AttachShadow(this.MessageDescriptions.GetAccessor(message)))); + // Test HTTP POST, with query string, but not using the Authorization header + message = CreateTestRequestTokenMessage( + this.MessageDescriptions, + new MessageReceivingEndpoint("https://www.google.com/accounts/OAuthGetRequestToken?scope=http://www.google.com/m8/feeds/", HttpDeliveryMethods.PostRequest)); + message.ExtraData.Remove("scope"); // remove it from ExtraData since we put it in the URL Assert.AreEqual( "GET&https%3A%2F%2Fwww.google.com%2Faccounts%2FOAuthGetRequestToken&oauth_consumer_key%3Dnerdbank.org%26oauth_nonce%3Dfe4045a3f0efdd1e019fa8f8ae3f5c38%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1222665749%26oauth_version%3D1.0%26scope%3Dhttp%253A%252F%252Fwww.google.com%252Fm8%252Ffeeds%252F", SigningBindingElementBase_Accessor.ConstructSignatureBaseString(message, MessageDictionary_Accessor.AttachShadow(this.MessageDescriptions.GetAccessor(message)))); } - internal static UnauthorizedTokenRequest CreateTestRequestTokenMessage(MessageDescriptionCollection messageDescriptions) { - MessageReceivingEndpoint endpoint = new MessageReceivingEndpoint("https://www.google.com/accounts/OAuthGetRequestToken", HttpDeliveryMethods.AuthorizationHeaderRequest | HttpDeliveryMethods.GetRequest); + internal static UnauthorizedTokenRequest CreateTestRequestTokenMessage(MessageDescriptionCollection messageDescriptions, MessageReceivingEndpoint endpoint) { + endpoint = endpoint ?? new MessageReceivingEndpoint("https://www.google.com/accounts/OAuthGetRequestToken", HttpDeliveryMethods.AuthorizationHeaderRequest | HttpDeliveryMethods.GetRequest); UnauthorizedTokenRequest message = new UnauthorizedTokenRequest(endpoint); message.ConsumerKey = "nerdbank.org"; ((ITamperResistantOAuthMessage)message).ConsumerSecret = "nerdbanksecret"; diff --git a/src/DotNetOpenAuth.Test/OpenId/Extensions/AttributeExchange/FetchResponseTests.cs b/src/DotNetOpenAuth.Test/OpenId/Extensions/AttributeExchange/FetchResponseTests.cs index d467186..9d209a9 100644 --- a/src/DotNetOpenAuth.Test/OpenId/Extensions/AttributeExchange/FetchResponseTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/Extensions/AttributeExchange/FetchResponseTests.cs @@ -4,7 +4,7 @@ // </copyright> //----------------------------------------------------------------------- -namespace DotNetOpenId.Test.OpenId.Extensions { +namespace DotNetOpenAuth.Test.OpenId.Extensions { using System; using System.IO; using DotNetOpenAuth.OpenId.Extensions.AttributeExchange; diff --git a/src/DotNetOpenAuth.Test/OpenId/IdentifierTests.cs b/src/DotNetOpenAuth.Test/OpenId/IdentifierTests.cs index 53df6c8..3e599e9 100644 --- a/src/DotNetOpenAuth.Test/OpenId/IdentifierTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/IdentifierTests.cs @@ -22,7 +22,7 @@ namespace DotNetOpenAuth.Test.OpenId { public void TryParseNoThrow() { Identifier id; Assert.IsFalse(Identifier.TryParse(null, out id)); - Assert.IsFalse(Identifier.TryParse("", out id)); + Assert.IsFalse(Identifier.TryParse(string.Empty, out id)); } [TestMethod] diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs index d5ba346..d1fc10b 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs @@ -7,9 +7,11 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { using System; using System.Collections.Generic; + using System.Collections.Specialized; using System.Diagnostics.Contracts; using System.Globalization; using System.Text; + using System.Web; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Bindings; using DotNetOpenAuth.Messaging.Reflection; @@ -164,13 +166,29 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { signatureBaseStringElements.Add(message.HttpMethod.ToUpperInvariant()); + var encodedDictionary = OAuthChannel.GetUriEscapedParameters(messageDictionary); + encodedDictionary.Remove("oauth_signature"); + if (message.Recipient.Query != null) { + // It seeems to me a deviation from the OAuth 1.0 spec to be willing to scrape the query + // for parameters on anything but GET requests, but Google does it so to interop we must + // as well. Besides, it seems more secure to sign everything if it's there. + NameValueCollection nvc = HttpUtility.ParseQueryString(message.Recipient.Query); + foreach (string key in nvc) { + encodedDictionary.Add(Uri.EscapeDataString(key), Uri.EscapeDataString(nvc[key])); + } + } else if (message.HttpMethod == "POST") { + // If the HttpWebRequest that we're sending out has a content-type header + // of application/x-www-form-urlencoded, we should be parsing out those parameters + // and adding them to this dictionary as well. + // But at this point we don't have access to the HttpWebRequest (design flaw?) + // TODO: figure this out. + } + UriBuilder endpoint = new UriBuilder(message.Recipient); endpoint.Query = null; endpoint.Fragment = null; signatureBaseStringElements.Add(endpoint.Uri.AbsoluteUri); - var encodedDictionary = OAuthChannel.GetUriEscapedParameters(messageDictionary); - encodedDictionary.Remove("oauth_signature"); var sortedKeyValueList = new List<KeyValuePair<string, string>>(encodedDictionary); sortedKeyValueList.Sort(SignatureBaseStringParameterComparer); StringBuilder paramBuilder = new StringBuilder(); diff --git a/src/DotNetOpenAuth/OAuth/ServiceProvider.cs b/src/DotNetOpenAuth/OAuth/ServiceProvider.cs index a8a702b..122e7ee 100644 --- a/src/DotNetOpenAuth/OAuth/ServiceProvider.cs +++ b/src/DotNetOpenAuth/OAuth/ServiceProvider.cs @@ -271,23 +271,44 @@ namespace DotNetOpenAuth.OAuth { /// <param name="request">The Consumer's original authorization request.</param> /// <returns> /// The message to send to the Consumer using <see cref="Channel"/> if one is necessary. - /// Null if the Consumer did not request a callback. + /// Null if the Consumer did not request a callback as part of the authorization request. /// </returns> [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Consistent user experience with instance.")] public UserAuthorizationResponse PrepareAuthorizationResponse(UserAuthorizationRequest request) { + Contract.Requires(request != null); ErrorUtilities.VerifyArgumentNotNull(request, "request"); if (request.Callback != null) { - var authorization = new UserAuthorizationResponse(request.Callback) { - RequestToken = request.RequestToken, - }; - return authorization; + return this.PrepareAuthorizationResponse(request, request.Callback); } else { return null; } } /// <summary> + /// Prepares the message to send back to the consumer following proper authorization of + /// a token by an interactive user at the Service Provider's web site. + /// </summary> + /// <param name="request">The Consumer's original authorization request.</param> + /// <param name="callback">The callback URI the consumer has previously registered + /// with this service provider.</param> + /// <returns> + /// The message to send to the Consumer using <see cref="Channel"/>. + /// </returns> + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Consistent user experience with instance.")] + public UserAuthorizationResponse PrepareAuthorizationResponse(UserAuthorizationRequest request, Uri callback) { + Contract.Requires(request != null); + Contract.Requires(callback != null); + ErrorUtilities.VerifyArgumentNotNull(request, "request"); + ErrorUtilities.VerifyArgumentNotNull(callback, "callback"); + + var authorization = new UserAuthorizationResponse(request.Callback) { + RequestToken = request.RequestToken, + }; + return authorization; + } + + /// <summary> /// Gets the incoming request to exchange an authorized token for an access token. /// </summary> /// <returns>The incoming request, or null if no OAuth message was attached.</returns> diff --git a/src/DotNetOpenAuth/OpenId/HmacShaAssociation.cs b/src/DotNetOpenAuth/OpenId/HmacShaAssociation.cs index d35dcd1..16d8f74 100644 --- a/src/DotNetOpenAuth/OpenId/HmacShaAssociation.cs +++ b/src/DotNetOpenAuth/OpenId/HmacShaAssociation.cs @@ -8,6 +8,7 @@ namespace DotNetOpenAuth.OpenId { using System; using System.Collections.Generic; using System.Diagnostics; + using System.Globalization; using System.Linq; using System.Security.Cryptography; using DotNetOpenAuth.Messaging; @@ -148,13 +149,19 @@ namespace DotNetOpenAuth.OpenId { ErrorUtilities.VerifyNonZeroLength(associationType, "associationType"); ErrorUtilities.VerifyArgumentNotNull(securitySettings, "securitySettings"); + int secretLength = GetSecretLength(protocol, associationType); + // Generate the handle. It must be unique, and preferably unpredictable, // so we use a time element and a random data element to generate it. string uniq = MessagingUtilities.GetCryptoRandomDataAsBase64(4); - string handle = "{" + associationType + "}{" + DateTime.UtcNow.Ticks + "}{" + uniq + "}"; + string handle = string.Format( + CultureInfo.InvariantCulture, + "{{{0}}}{{{1}}}{{{2}}}", + DateTime.UtcNow.Ticks, + uniq, + secretLength); // Generate the secret that will be used for signing - int secretLength = GetSecretLength(protocol, associationType); byte[] secret = MessagingUtilities.GetCryptoRandomData(secretLength); TimeSpan lifetime; diff --git a/src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs b/src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs index f9f638a..6a5c0a8 100644 --- a/src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs +++ b/src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs @@ -40,7 +40,7 @@ namespace DotNetOpenAuth.OpenId.Messages { /// </summary> /// <value>Value: A valid association session type from Section 8.4 (Association Session Types). </value> /// <remarks>Note: Unless using transport layer encryption, "no-encryption" MUST NOT be used. </remarks> - [MessagePart("openid.session_type", IsRequired = true, AllowEmpty = true)] + [MessagePart("openid.session_type", IsRequired = false, AllowEmpty = true)] [MessagePart("openid.session_type", IsRequired = true, AllowEmpty = false, MinVersion = "2.0")] internal string SessionType { get; set; } |