diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2012-12-02 15:57:23 -0800 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2012-12-02 15:57:23 -0800 |
commit | c7509b6d29ffe07098d0c2b81acbd1af34e1c076 (patch) | |
tree | 058db773e244038713f121b9381a962eca327ce3 | |
parent | 67f3936edd8ed795f16e596f349a2f12712810e9 (diff) | |
parent | 84ef4b74365fe78156792cf17bb945a9ace4e03f (diff) | |
download | DotNetOpenAuth-c7509b6d29ffe07098d0c2b81acbd1af34e1c076.zip DotNetOpenAuth-c7509b6d29ffe07098d0c2b81acbd1af34e1c076.tar.gz DotNetOpenAuth-c7509b6d29ffe07098d0c2b81acbd1af34e1c076.tar.bz2 |
Merge branch 'v4.1'
Conflicts:
src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/StandardAccessTokenAnalyzer.cs
src/DotNetOpenAuth.Test/OAuth2/OAuth2TestBase.cs
src/version.txt
26 files changed, 338 insertions, 38 deletions
diff --git a/samples/OAuthAuthorizationServer/OAuthAuthorizationServer.csproj b/samples/OAuthAuthorizationServer/OAuthAuthorizationServer.csproj index 9f810bb..8c7fd08 100644 --- a/samples/OAuthAuthorizationServer/OAuthAuthorizationServer.csproj +++ b/samples/OAuthAuthorizationServer/OAuthAuthorizationServer.csproj @@ -63,6 +63,10 @@ <RequiredTargetFramework>3.5</RequiredTargetFramework> </Reference> <Reference Include="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" /> + <Reference Include="System.Net.Http, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\..\lib\net-v4.0\System.Net.Http.dll</HintPath> + </Reference> <Reference Include="System.Xml.Linq"> <RequiredTargetFramework>3.5</RequiredTargetFramework> </Reference> diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth/AuthenticationOnlyCookieOAuthTokenManager.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth/AuthenticationOnlyCookieOAuthTokenManager.cs index 2ec988b..efc382f 100644 --- a/src/DotNetOpenAuth.AspNet/Clients/OAuth/AuthenticationOnlyCookieOAuthTokenManager.cs +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth/AuthenticationOnlyCookieOAuthTokenManager.cs @@ -17,7 +17,7 @@ namespace DotNetOpenAuth.AspNet.Clients { /// <summary> /// Key used for token cookie /// </summary> - private const string TokenCookieKey = "OAuthTokenSecret"; + protected const string TokenCookieKey = "OAuthTokenSecret"; /// <summary> /// Primary request context. @@ -41,7 +41,7 @@ namespace DotNetOpenAuth.AspNet.Clients { /// <summary> /// Gets the effective HttpContext object to use. /// </summary> - private HttpContextBase Context { + protected HttpContextBase Context { get { return this.primaryContext ?? new HttpContextWrapper(HttpContext.Current); } @@ -54,15 +54,13 @@ namespace DotNetOpenAuth.AspNet.Clients { /// <returns> /// The token's secret /// </returns> - public string GetTokenSecret(string token) { + public virtual string GetTokenSecret(string token) { HttpCookie cookie = this.Context.Request.Cookies[TokenCookieKey]; if (cookie == null || string.IsNullOrEmpty(cookie.Values[token])) { return null; } - byte[] cookieBytes = HttpServerUtility.UrlTokenDecode(cookie.Values[token]); - byte[] clearBytes = MachineKeyUtil.Unprotect(cookieBytes, TokenCookieKey, "Token:" + token); - string secret = Encoding.UTF8.GetString(clearBytes); + string secret = DecodeAndUnprotectToken(token, cookie.Values[token]); return secret; } @@ -72,7 +70,7 @@ namespace DotNetOpenAuth.AspNet.Clients { /// <param name="requestToken">The request token.</param> /// <param name="accessToken">The access token.</param> /// <param name="accessTokenSecret">The access token secret.</param> - public void ReplaceRequestTokenWithAccessToken(string requestToken, string accessToken, string accessTokenSecret) { + public virtual void ReplaceRequestTokenWithAccessToken(string requestToken, string accessToken, string accessTokenSecret) { var cookie = new HttpCookie(TokenCookieKey) { Value = string.Empty, Expires = DateTime.UtcNow.AddDays(-5) @@ -85,7 +83,7 @@ namespace DotNetOpenAuth.AspNet.Clients { /// </summary> /// <param name="requestToken">The request token.</param> /// <param name="requestTokenSecret">The request token secret.</param> - public void StoreRequestToken(string requestToken, string requestTokenSecret) { + public virtual void StoreRequestToken(string requestToken, string requestTokenSecret) { var cookie = new HttpCookie(TokenCookieKey) { HttpOnly = true }; @@ -94,10 +92,36 @@ namespace DotNetOpenAuth.AspNet.Clients { cookie.Secure = true; } - byte[] cookieBytes = Encoding.UTF8.GetBytes(requestTokenSecret); - var secretBytes = MachineKeyUtil.Protect(cookieBytes, TokenCookieKey, "Token:" + requestToken); - cookie.Values[requestToken] = HttpServerUtility.UrlTokenEncode(secretBytes); + var encryptedToken = ProtectAndEncodeToken(requestToken, requestTokenSecret); + cookie.Values[requestToken] = encryptedToken; + this.Context.Response.Cookies.Set(cookie); } + + /// <summary> + /// Protect and url-encode the specified token secret. + /// </summary> + /// <param name="token">The token to be used as a key.</param> + /// <param name="tokenSecret">The token secret to be protected</param> + /// <returns>The encrypted and protected string.</returns> + protected static string ProtectAndEncodeToken(string token, string tokenSecret) + { + byte[] cookieBytes = Encoding.UTF8.GetBytes(tokenSecret); + var secretBytes = MachineKeyUtil.Protect(cookieBytes, TokenCookieKey, "Token:" + token); + return HttpServerUtility.UrlTokenEncode(secretBytes); + } + + /// <summary> + /// Url-decode and unprotect the specified encrypted token string. + /// </summary> + /// <param name="token">The token to be used as a key.</param> + /// <param name="encryptedToken">The encrypted token to be decrypted</param> + /// <returns>The original token secret</returns> + protected static string DecodeAndUnprotectToken(string token, string encryptedToken) + { + byte[] cookieBytes = HttpServerUtility.UrlTokenDecode(encryptedToken); + byte[] clearBytes = MachineKeyUtil.Unprotect(cookieBytes, TokenCookieKey, "Token:" + token); + return Encoding.UTF8.GetString(clearBytes); + } } }
\ No newline at end of file diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth/CookieOAuthTokenManager.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth/CookieOAuthTokenManager.cs new file mode 100644 index 0000000..398ee85 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth/CookieOAuthTokenManager.cs @@ -0,0 +1,79 @@ +//----------------------------------------------------------------------- +// <copyright file="CookieOAuthTokenManager.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System.Web; + using System.Web.Security; + + /// <summary> + /// Stores OAuth tokens in the current request's cookie. + /// </summary> + /// <remarks> + /// This class is different from the <see cref="AuthenticationOnlyCookieOAuthTokenManager"/> in that + /// it also stores the access token after the authentication has succeeded. + /// </remarks> + public class CookieOAuthTokenManager : AuthenticationOnlyCookieOAuthTokenManager { + /// <summary> + /// Initializes a new instance of the <see cref="CookieOAuthTokenManager"/> class. + /// </summary> + public CookieOAuthTokenManager() { + } + + /// <summary> + /// Initializes a new instance of the <see cref="CookieOAuthTokenManager"/> class. + /// </summary> + /// <param name="context">The current request context.</param> + public CookieOAuthTokenManager(HttpContextBase context) + : base(context) { + } + + /// <summary> + /// Gets the token secret from the specified token. + /// </summary> + /// <param name="token">The token.</param> + /// <returns> + /// The token's secret + /// </returns> + public override string GetTokenSecret(string token) { + string secret = base.GetTokenSecret(token); + if (secret != null) { + return secret; + } + + // The base class checks for cookies in the Request object. + // Here we check in the Response object as well because we + // may have set it earlier in the request life cycle. + HttpCookie cookie = this.Context.Response.Cookies[TokenCookieKey]; + if (cookie == null || string.IsNullOrEmpty(cookie.Values[token])) { + return null; + } + + secret = DecodeAndUnprotectToken(token, cookie.Values[token]); + return secret; + } + + /// <summary> + /// Replaces the request token with access token. + /// </summary> + /// <param name="requestToken">The request token.</param> + /// <param name="accessToken">The access token.</param> + /// <param name="accessTokenSecret">The access token secret.</param> + public override void ReplaceRequestTokenWithAccessToken(string requestToken, string accessToken, string accessTokenSecret) { + var cookie = new HttpCookie(TokenCookieKey) { + HttpOnly = true + }; + + if (FormsAuthentication.RequireSSL) { + cookie.Secure = true; + } + + var encryptedToken = ProtectAndEncodeToken(accessToken, accessTokenSecret); + cookie.Values[accessToken] = encryptedToken; + + this.Context.Response.Cookies.Set(cookie); + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth/DotNetOpenAuthWebConsumer.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth/DotNetOpenAuthWebConsumer.cs index 7eda8e4..9af6804 100644 --- a/src/DotNetOpenAuth.AspNet/Clients/OAuth/DotNetOpenAuthWebConsumer.cs +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth/DotNetOpenAuthWebConsumer.cs @@ -77,7 +77,7 @@ namespace DotNetOpenAuth.AspNet.Clients { /// The callback. /// </param> public void RequestAuthentication(Uri callback) { - var redirectParameters = new Dictionary<string, string> { { "force_login", "false" } }; + var redirectParameters = new Dictionary<string, string>(); UserAuthorizationRequest request = this.webConsumer.PrepareRequestUserAuthorization( callback, null, redirectParameters); this.webConsumer.Channel.PrepareResponse(request).Send(); diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth/LinkedInClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth/LinkedInClient.cs index 26bc88d..3c157f3 100644 --- a/src/DotNetOpenAuth.AspNet/Clients/OAuth/LinkedInClient.cs +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth/LinkedInClient.cs @@ -60,7 +60,7 @@ namespace DotNetOpenAuth.AspNet.Clients { [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "We can't dispose the object because we still need it through the app lifetime.")] public LinkedInClient(string consumerKey, string consumerSecret) - : this(consumerKey, consumerSecret, new AuthenticationOnlyCookieOAuthTokenManager()) { } + : this(consumerKey, consumerSecret, new CookieOAuthTokenManager()) { } /// <summary> /// Initializes a new instance of the <see cref="LinkedInClient"/> class. diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/MicrosoftClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/MicrosoftClient.cs index 5ac5591..faea868 100644 --- a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/MicrosoftClient.cs +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/MicrosoftClient.cs @@ -20,12 +20,12 @@ namespace DotNetOpenAuth.AspNet.Clients { /// <summary> /// The authorization endpoint. /// </summary> - private const string AuthorizationEndpoint = "https://oauth.live.com/authorize"; + private const string AuthorizationEndpoint = "https://login.live.com/oauth20_authorize.srf"; /// <summary> /// The token endpoint. /// </summary> - private const string TokenEndpoint = "https://oauth.live.com/token"; + private const string TokenEndpoint = "https://login.live.com/oauth20_token.srf"; /// <summary> /// The _app id. diff --git a/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj b/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj index 2f03ec7..405ac3c 100644 --- a/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj +++ b/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj @@ -28,7 +28,7 @@ <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.props" /> <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.Product.props" /> <PropertyGroup> - <RootNamespace>DotNetOpenAuth.AspNet</RootNamespace> + <RootNamespace>DotNetOpenAuth.AspNet</RootNamespace> </PropertyGroup> <ItemGroup> <Reference Include="System" /> @@ -48,6 +48,7 @@ <Compile Include="Clients\OAuth\AuthenticationOnlyCookieOAuthTokenManager.cs"> <SubType>Code</SubType> </Compile> + <Compile Include="Clients\OAuth\CookieOAuthTokenManager.cs" /> <Compile Include="Clients\OAuth\IOAuthTokenManager.cs" /> <Compile Include="Clients\OAuth\SimpleConsumerTokenManager.cs" /> <Compile Include="IAuthenticationClient.cs" /> diff --git a/src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs b/src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs index 377e6d8..e7ac254 100644 --- a/src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs +++ b/src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs @@ -215,8 +215,8 @@ namespace DotNetOpenAuth.Messaging { if (this.signed) { using (var dataStream = new MemoryStream(data)) { var dataReader = new BinaryReader(dataStream); - signature = dataReader.ReadBuffer(); - data = dataReader.ReadBuffer(); + signature = dataReader.ReadBuffer(1024); + data = dataReader.ReadBuffer(8 * 1024); } // Verify that the verification code was issued by message authorization server. diff --git a/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.Designer.cs b/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.Designer.cs index 2fe273f..4f89589 100644 --- a/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.Designer.cs +++ b/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:4.0.30319.239 +// Runtime Version:4.0.30319.18010 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -106,6 +106,15 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Looks up a localized string similar to Decoding failed due to data corruption.. + /// </summary> + internal static string DataCorruptionDetected { + get { + return ResourceManager.GetString("DataCorruptionDetected", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to An instance of type {0} was expected, but received unexpected derived type {1}.. /// </summary> internal static string DerivedTypeNotExpected { diff --git a/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.resx b/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.resx index fbdb63d..15ca046 100644 --- a/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.resx +++ b/src/DotNetOpenAuth.Core/Messaging/MessagingStrings.resx @@ -112,10 +112,10 @@ <value>2.0</value> </resheader> <resheader name="reader"> - <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <resheader name="writer"> - <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="ArgumentPropertyMissing" xml:space="preserve"> <value>Argument's {0}.{1} property is required but is empty or null.</value> @@ -333,4 +333,7 @@ <data name="UnexpectedBufferLength" xml:space="preserve"> <value>Unexpected buffer length.</value> </data> + <data name="DataCorruptionDetected" xml:space="preserve"> + <value>Decoding failed due to data corruption.</value> + </data> </root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs index d6df26c..2e72c97 100644 --- a/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs @@ -171,9 +171,10 @@ namespace DotNetOpenAuth.Messaging { /// <param name="outgoingResponse">The response to send to the user agent.</param> /// <returns>The <see cref="HttpResponseMessage"/> instance to be returned by the Web API method.</returns> public static HttpResponseMessage AsHttpResponseMessage(this OutgoingWebResponse outgoingResponse) { - HttpResponseMessage response = new HttpResponseMessage(outgoingResponse.Status) { - Content = new StreamContent(outgoingResponse.ResponseStream) - }; + HttpResponseMessage response = new HttpResponseMessage(outgoingResponse.Status); + if (outgoingResponse.ResponseStream != null) { + response.Content = new StreamContent(outgoingResponse.ResponseStream); + } var responseHeaders = outgoingResponse.Headers; foreach (var header in responseHeaders.AllKeys) { @@ -876,7 +877,7 @@ namespace DotNetOpenAuth.Messaging { using (var encryptedStream = new MemoryStream(buffer)) { var encryptedStreamReader = new BinaryReader(encryptedStream); - byte[] encryptedPrequel = encryptedStreamReader.ReadBytes(encryptedStreamReader.ReadInt32()); + byte[] encryptedPrequel = encryptedStreamReader.ReadBuffer(4096); byte[] prequel = crypto.Decrypt(encryptedPrequel, false); using (var symmetricCrypto = new RijndaelManaged()) { @@ -1068,7 +1069,7 @@ namespace DotNetOpenAuth.Messaging { missingPaddingCharacters = 0; break; default: - throw ErrorUtilities.ThrowInternal("No more than two padding characters should be present for base64."); + throw new ProtocolException(MessagingStrings.DataCorruptionDetected, new ArgumentException("No more than two padding characters should be present for base64.")); } var builder = new StringBuilder(base64WebSafe, base64WebSafe.Length + missingPaddingCharacters); builder.Replace('-', '+').Replace('_', '/'); @@ -1746,10 +1747,17 @@ namespace DotNetOpenAuth.Messaging { /// Reads a buffer that is prefixed with its own length. /// </summary> /// <param name="reader">The binary reader positioned at the buffer length.</param> + /// <param name="maxBufferSize"> + /// The maximum size of the buffer that should be permitted. + /// Although the stream will indicate the size of the buffer, this mitigates data corruption + /// or DoS attacks causing the web server to allocate too much memory for a small data packet. + /// </param> /// <returns>The read buffer.</returns> - internal static byte[] ReadBuffer(this BinaryReader reader) { + internal static byte[] ReadBuffer(this BinaryReader reader, int maxBufferSize) { Requires.NotNull(reader, "reader"); + Requires.InRange(maxBufferSize > 0 && maxBufferSize < 1024 * 1024, "maxBufferSize"); int length = reader.ReadInt32(); + ErrorUtilities.VerifyProtocol(length <= maxBufferSize, MessagingStrings.DataCorruptionDetected); byte[] buffer = new byte[length]; ErrorUtilities.VerifyProtocol(reader.Read(buffer, 0, length) == length, MessagingStrings.UnexpectedBufferLength); return buffer; diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthServerStrings.Designer.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthServerStrings.Designer.cs index 4b4f830..8941a94 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthServerStrings.Designer.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthServerStrings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:4.0.30319.17614 +// Runtime Version:4.0.30319.18010 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -70,6 +70,15 @@ namespace DotNetOpenAuth.OAuth2 { } /// <summary> + /// Looks up a localized string similar to The access token's private signing key must be set.. + /// </summary> + internal static string AccessTokenSigningKeyMissing { + get { + return ResourceManager.GetString("AccessTokenSigningKeyMissing", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to The callback URL ({0}) is not allowed for this client.. /// </summary> internal static string ClientCallbackDisallowed { diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthServerStrings.resx b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthServerStrings.resx index 29d841a..8aaa567 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthServerStrings.resx +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthServerStrings.resx @@ -112,14 +112,17 @@ <value>2.0</value> </resheader> <resheader name="reader"> - <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <resheader name="writer"> - <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="AccessScopeExceedsGrantScope" xml:space="preserve"> <value>The requested access scope exceeds the grant scope.</value> </data> + <data name="AccessTokenSigningKeyMissing" xml:space="preserve"> + <value>The access token's private signing key must be set.</value> + </data> <data name="ClientCallbackDisallowed" xml:space="preserve"> <value>The callback URL ({0}) is not allowed for this client.</value> </data> diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs index 7d829c5..050a4ab 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs @@ -10,6 +10,9 @@ namespace DotNetOpenAuth.OAuth2 { using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; +#if CLR4 + using System.Net.Http; +#endif using System.Security.Cryptography; using System.Text; using System.Web; @@ -129,6 +132,17 @@ namespace DotNetOpenAuth.OAuth2 { this.Channel.Respond(response); } +#if CLR4 + /// <summary> + /// Handles an incoming request to the authorization server's token endpoint. + /// </summary> + /// <param name="request">The HTTP request.</param> + /// <returns>The HTTP response to send to the client.</returns> + public OutgoingWebResponse HandleTokenRequest(HttpRequestMessage request) { + return this.HandleTokenRequest(new HttpRequestInfo(request)); + } +#endif + /// <summary> /// Handles an incoming request to the authorization server's token endpoint. /// </summary> diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServerAccessToken.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServerAccessToken.cs index c577a0a..a127166 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServerAccessToken.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServerAccessToken.cs @@ -45,6 +45,7 @@ namespace DotNetOpenAuth.OAuth2 { /// </summary> /// <returns>A non-empty string.</returns> protected internal override string Serialize() { + ErrorUtilities.VerifyHost(this.AccessTokenSigningKey != null, AuthServerStrings.AccessTokenSigningKeyMissing); var formatter = CreateFormatter(this.AccessTokenSigningKey, this.ResourceServerEncryptionKey); return formatter.Serialize(this); } diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientCredentialHttpBasicReader.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientCredentialHttpBasicReader.cs index 655d38f..6f0bbc4 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientCredentialHttpBasicReader.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientCredentialHttpBasicReader.cs @@ -7,6 +7,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { using System; using System.Collections.Generic; + using System.Globalization; using System.Linq; using System.Text; using System.Web; @@ -21,10 +22,15 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { /// Gets this module's contribution to an HTTP 401 WWW-Authenticate header so the client knows what kind of authentication this module supports. /// </summary> public override string AuthenticateHeader { - get { return "Basic"; } + get { return string.Format(CultureInfo.InvariantCulture, "Basic realm=\"{0}\"", this.Realm); } } /// <summary> + /// Gets or sets the realm that is included in an HTTP WWW-Authenticate header included in a 401 Unauthorized response. + /// </summary> + public string Realm { get; set; } + + /// <summary> /// Attempts to extract client identification/authentication information from a message. /// </summary> /// <param name="authorizationServerHost">The authorization server host.</param> diff --git a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientBase.cs b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientBase.cs index 822ce6d..d4ea171 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientBase.cs +++ b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientBase.cs @@ -126,7 +126,7 @@ namespace DotNetOpenAuth.OAuth2 { Requires.NotNull(requestHeaders, "requestHeaders"); Requires.NotNull(authorization, "authorization"); Requires.True(!string.IsNullOrEmpty(authorization.AccessToken), "authorization"); - ErrorUtilities.VerifyProtocol(!authorization.AccessTokenExpirationUtc.HasValue || authorization.AccessTokenExpirationUtc < DateTime.UtcNow || authorization.RefreshToken != null, ClientStrings.AuthorizationExpired); + ErrorUtilities.VerifyProtocol(!authorization.AccessTokenExpirationUtc.HasValue || authorization.AccessTokenExpirationUtc >= DateTime.UtcNow || authorization.RefreshToken != null, ClientStrings.AuthorizationExpired); if (authorization.AccessTokenExpirationUtc.HasValue && authorization.AccessTokenExpirationUtc.Value < DateTime.UtcNow) { ErrorUtilities.VerifyProtocol(authorization.RefreshToken != null, ClientStrings.AccessTokenRefreshFailed); diff --git a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientCredentialApplicator.cs b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientCredentialApplicator.cs index cc4e45f..0677f5a 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientCredentialApplicator.cs +++ b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientCredentialApplicator.cs @@ -135,7 +135,9 @@ namespace DotNetOpenAuth.OAuth2 { clientIdentifier); } - request.Credentials = this.credential ?? new NetworkCredential(clientIdentifier, this.clientSecret); + // HttpWebRequest ignores the Credentials property until the remote server returns a 401 Unauthorized. + // So we also set the HTTP Authorization request header directly. + OAuthUtilities.ApplyHttpBasicAuth(request.Headers, clientIdentifier, this.clientSecret); } } } diff --git a/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/StandardAccessTokenAnalyzer.cs b/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/StandardAccessTokenAnalyzer.cs index a03bdab..62436ec 100644 --- a/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/StandardAccessTokenAnalyzer.cs +++ b/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/StandardAccessTokenAnalyzer.cs @@ -8,6 +8,7 @@ namespace DotNetOpenAuth.OAuth2 { using System; using System.Collections.Generic; using System.Diagnostics.Contracts; + using System.IO; using System.Security.Cryptography; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OAuth2.ChannelElements; @@ -23,8 +24,7 @@ namespace DotNetOpenAuth.OAuth2 { /// <param name="resourceServerPrivateEncryptionKey">The crypto service provider with the resource server private encryption key.</param> public StandardAccessTokenAnalyzer(RSACryptoServiceProvider authorizationServerPublicSigningKey, RSACryptoServiceProvider resourceServerPrivateEncryptionKey) { Requires.NotNull(authorizationServerPublicSigningKey, "authorizationServerPublicSigningKey"); - Requires.NotNull(resourceServerPrivateEncryptionKey, "resourceServerPrivateEncryptionKey"); - Requires.True(!resourceServerPrivateEncryptionKey.PublicOnly, "resourceServerPrivateEncryptionKey"); + Requires.True(resourceServerPrivateEncryptionKey == null || !resourceServerPrivateEncryptionKey.PublicOnly, "resourceServerPrivateEncryptionKey"); this.AuthorizationServerPublicSigningKey = authorizationServerPublicSigningKey; this.ResourceServerPrivateEncryptionKey = resourceServerPrivateEncryptionKey; } @@ -49,9 +49,15 @@ namespace DotNetOpenAuth.OAuth2 { /// <returns>The deserialized, validated token.</returns> /// <exception cref="ProtocolException">Thrown if the access token is expired, invalid, or from an untrusted authorization server.</exception> public virtual AccessToken DeserializeAccessToken(IDirectedProtocolMessage message, string accessToken) { + ErrorUtilities.VerifyProtocol(!string.IsNullOrEmpty(accessToken), ResourceServerStrings.MissingAccessToken); var accessTokenFormatter = AccessToken.CreateFormatter(this.AuthorizationServerPublicSigningKey, this.ResourceServerPrivateEncryptionKey); var token = new AccessToken(); - accessTokenFormatter.Deserialize(token, accessToken, message, Protocol.access_token); + try { + accessTokenFormatter.Deserialize(token, accessToken, message, Protocol.access_token); + } catch (IOException ex) { + throw new ProtocolException(ResourceServerStrings.InvalidAccessToken, ex); + } + return token; } } diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AssociationDataBag.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AssociationDataBag.cs index ee48670..f619b76 100644 --- a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AssociationDataBag.cs +++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/AssociationDataBag.cs @@ -72,7 +72,7 @@ namespace DotNetOpenAuth.OpenId.Provider { public void Deserialize(Stream stream) { var reader = new BinaryReader(stream); this.IsPrivateAssociation = reader.ReadBoolean(); - this.Secret = reader.ReadBuffer(); + this.Secret = reader.ReadBuffer(256); this.ExpiresUtc = TimestampEncoder.Epoch + TimeSpan.FromSeconds(reader.ReadInt32()); } diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty.UI/OpenId/RelyingParty/SelectorButton.cs b/src/DotNetOpenAuth.OpenId.RelyingParty.UI/OpenId/RelyingParty/SelectorButton.cs index b4d0aa0..670189c 100644 --- a/src/DotNetOpenAuth.OpenId.RelyingParty.UI/OpenId/RelyingParty/SelectorButton.cs +++ b/src/DotNetOpenAuth.OpenId.RelyingParty.UI/OpenId/RelyingParty/SelectorButton.cs @@ -13,6 +13,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// A button that would appear in the <see cref="OpenIdSelector"/> control via its <see cref="OpenIdSelector.Buttons"/> collection. /// </summary> [ContractClass(typeof(SelectorButtonContract))] + [Serializable] public abstract class SelectorButton { /// <summary> /// Initializes a new instance of the <see cref="SelectorButton"/> class. diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs index fa7768b..912a322 100644 --- a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs @@ -195,6 +195,11 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { try { if (cryptoKey == null) { cryptoKey = this.cryptoKeyStore.GetKey(SecretUri.AbsoluteUri, returnToParameters[ReturnToSignatureHandleParameterName]); + ErrorUtilities.VerifyProtocol( + cryptoKey != null, + MessagingStrings.MissingDecryptionKeyForHandle, + SecretUri.AbsoluteUri, + returnToParameters[ReturnToSignatureHandleParameterName]); } using (var signer = HmacAlgorithms.Create(HmacAlgorithms.HmacSha256, cryptoKey.Key)) { diff --git a/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj b/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj index b58aa17..189a569 100644 --- a/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj +++ b/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj @@ -249,6 +249,7 @@ <Compile Include="Mocks\TestMessageFactory.cs" /> <Compile Include="OAuth2\AuthorizationServerTests.cs" /> <Compile Include="OAuth2\MessageFactoryTests.cs" /> + <Compile Include="OAuth2\ResourceServerTests.cs" /> <Compile Include="OAuth2\UserAgentClientAuthorizeTests.cs" /> <Compile Include="OAuth2\OAuth2Coordinator.cs" /> <Compile Include="OAuth2\OAuth2TestBase.cs" /> diff --git a/src/DotNetOpenAuth.Test/Messaging/MessagingUtilitiesTests.cs b/src/DotNetOpenAuth.Test/Messaging/MessagingUtilitiesTests.cs index 0610018..cf0f9ca 100644 --- a/src/DotNetOpenAuth.Test/Messaging/MessagingUtilitiesTests.cs +++ b/src/DotNetOpenAuth.Test/Messaging/MessagingUtilitiesTests.cs @@ -91,6 +91,17 @@ namespace DotNetOpenAuth.Test.Messaging { } [Test] + public void AsHttpResponseMessageNoContent() { + var outgoingResponse = new OutgoingWebResponse(); + outgoingResponse.Headers.Add("X-SOME-HEADER", "value"); + + var httpResponseMessage = outgoingResponse.AsHttpResponseMessage(); + Assert.That(httpResponseMessage, Is.Not.Null); + Assert.That(httpResponseMessage.Headers.GetValues("X-SOME-HEADER").ToList(), Is.EqualTo(new[] { "value" })); + Assert.That(httpResponseMessage.Content, Is.Null); + } + + [Test] public void ToDictionary() { NameValueCollection nvc = new NameValueCollection(); nvc["a"] = "b"; diff --git a/src/DotNetOpenAuth.Test/OAuth2/OAuth2TestBase.cs b/src/DotNetOpenAuth.Test/OAuth2/OAuth2TestBase.cs index b8b930c..395b18c 100644 --- a/src/DotNetOpenAuth.Test/OAuth2/OAuth2TestBase.cs +++ b/src/DotNetOpenAuth.Test/OAuth2/OAuth2TestBase.cs @@ -8,6 +8,7 @@ namespace DotNetOpenAuth.Test.OAuth2 { using System; using System.Collections.Generic; using System.Linq; + using System.Security.Cryptography; using System.Text; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Bindings; @@ -29,6 +30,8 @@ namespace DotNetOpenAuth.Test.OAuth2 { protected static readonly Uri ClientCallback = new Uri("http://client/callback"); + protected static readonly RSACryptoServiceProvider AsymmetricKey = new RSACryptoServiceProvider(512); + protected static readonly AuthorizationServerDescription AuthorizationServerDescription = new AuthorizationServerDescription { AuthorizationEndpoint = new Uri("https://authserver/authorize"), TokenEndpoint = new Uri("https://authserver/token"), @@ -56,7 +59,7 @@ namespace DotNetOpenAuth.Test.OAuth2 { authHostMock .Setup(m => m.CheckAuthorizeResourceOwnerCredentialGrant(ResourceOwnerUsername, ResourceOwnerPassword, It.IsAny<IAccessTokenRequest>())) .Returns<string, string, IAccessTokenRequest>((p1, p2, p3) => new AutomatedUserAuthorizationCheckResponse(p3, true, ResourceOwnerUsername)); - authHostMock.Setup(m => m.CreateAccessToken(It.IsAny<IAccessTokenRequest>())).Returns(new AccessTokenResult(new AuthorizationServerAccessToken())); + authHostMock.Setup(m => m.CreateAccessToken(It.IsAny<IAccessTokenRequest>())).Returns(new AccessTokenResult(new AuthorizationServerAccessToken() { AccessTokenSigningKey = AsymmetricKey })); return authHostMock; } } diff --git a/src/DotNetOpenAuth.Test/OAuth2/ResourceServerTests.cs b/src/DotNetOpenAuth.Test/OAuth2/ResourceServerTests.cs new file mode 100644 index 0000000..80a8392 --- /dev/null +++ b/src/DotNetOpenAuth.Test/OAuth2/ResourceServerTests.cs @@ -0,0 +1,110 @@ +//----------------------------------------------------------------------- +// <copyright file="ResourceServerTests.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Test.OAuth2 { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Linq; + using System.Security.Cryptography; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth2; + using DotNetOpenAuth.OAuth2.ChannelElements; + using DotNetOpenAuth.OAuth2.Messages; + using Moq; + using NUnit.Framework; + + [TestFixture] + public class ResourceServerTests : OAuth2TestBase { + [Test] + public void GetAccessTokenWithMissingAccessToken() { + var resourceServer = new ResourceServer(new StandardAccessTokenAnalyzer(AsymmetricKey, null)); + + var requestHeaders = new NameValueCollection { + { "Authorization", "Bearer " }, + }; + var request = new HttpRequestInfo("GET", new Uri("http://localhost/resource"), headers: requestHeaders); + Assert.That(() => resourceServer.GetAccessToken(request), Throws.InstanceOf<ProtocolException>()); + } + + [Test] + public void GetPrincipalWithMissingAccessToken() { + var resourceServer = new ResourceServer(new StandardAccessTokenAnalyzer(AsymmetricKey, null)); + + var requestHeaders = new NameValueCollection { + { "Authorization", "Bearer " }, + }; + var request = new HttpRequestInfo("GET", new Uri("http://localhost/resource"), headers: requestHeaders); + Assert.That(() => resourceServer.GetPrincipal(request), Throws.InstanceOf<ProtocolException>()); + } + + [Test] + public void GetAccessTokenWithTotallyFakeToken() { + var resourceServer = new ResourceServer(new StandardAccessTokenAnalyzer(AsymmetricKey, null)); + + var requestHeaders = new NameValueCollection { + { "Authorization", "Bearer foobar" }, + }; + var request = new HttpRequestInfo("GET", new Uri("http://localhost/resource"), headers: requestHeaders); + Assert.That(() => resourceServer.GetAccessToken(request), Throws.InstanceOf<ProtocolException>()); + } + + [Test] + public void GetAccessTokenWithCorruptedToken() { + var accessToken = this.ObtainValidAccessToken(); + + var resourceServer = new ResourceServer(new StandardAccessTokenAnalyzer(AsymmetricKey, null)); + + var requestHeaders = new NameValueCollection { + { "Authorization", "Bearer " + accessToken.Substring(0, accessToken.Length - 1) + "zzz" }, + }; + var request = new HttpRequestInfo("GET", new Uri("http://localhost/resource"), headers: requestHeaders); + Assert.That(() => resourceServer.GetAccessToken(request), Throws.InstanceOf<ProtocolException>()); + } + + [Test] + public void GetAccessTokenWithValidToken() { + var accessToken = this.ObtainValidAccessToken(); + + var resourceServer = new ResourceServer(new StandardAccessTokenAnalyzer(AsymmetricKey, null)); + + var requestHeaders = new NameValueCollection { + { "Authorization", "Bearer " + accessToken }, + }; + var request = new HttpRequestInfo("GET", new Uri("http://localhost/resource"), headers: requestHeaders); + var resourceServerDecodedToken = resourceServer.GetAccessToken(request); + Assert.That(resourceServerDecodedToken, Is.Not.Null); + } + + private string ObtainValidAccessToken() { + string accessToken = null; + var authServer = CreateAuthorizationServerMock(); + authServer.Setup( + a => a.IsAuthorizationValid(It.Is<IAuthorizationDescription>(d => d.User == null && d.ClientIdentifier == ClientId && MessagingUtilities.AreEquivalent(d.Scope, TestScopes)))) + .Returns(true); + authServer.Setup( + a => a.CheckAuthorizeClientCredentialsGrant(It.Is<IAccessTokenRequest>(d => d.ClientIdentifier == ClientId && MessagingUtilities.AreEquivalent(d.Scope, TestScopes)))) + .Returns<IAccessTokenRequest>(req => new AutomatedAuthorizationCheckResponse(req, true)); + var coordinator = new OAuth2Coordinator<WebServerClient>( + AuthorizationServerDescription, + authServer.Object, + new WebServerClient(AuthorizationServerDescription), + client => { + var authState = client.GetClientAccessToken(TestScopes); + Assert.That(authState.AccessToken, Is.Not.Null.And.Not.Empty); + Assert.That(authState.RefreshToken, Is.Null); + accessToken = authState.AccessToken; + }, + server => { + server.HandleTokenRequest().Respond(); + }); + coordinator.Run(); + + return accessToken; + } + } +} |