diff options
Diffstat (limited to 'src/DotNetOpenAuth.AspNet')
13 files changed, 557 insertions, 42 deletions
diff --git a/src/DotNetOpenAuth.AspNet/AuthenticationResult.cs b/src/DotNetOpenAuth.AspNet/AuthenticationResult.cs index d5fb2d1..9e8492d 100644 --- a/src/DotNetOpenAuth.AspNet/AuthenticationResult.cs +++ b/src/DotNetOpenAuth.AspNet/AuthenticationResult.cs @@ -37,12 +37,24 @@ namespace DotNetOpenAuth.AspNet { /// The exception. /// </param> public AuthenticationResult(Exception exception) - : this(isSuccessful: false) { - if (exception == null) { + : this(exception, provider: null) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationResult"/> class. + /// </summary> + /// <param name="exception">The exception.</param> + /// <param name="provider">The provider name.</param> + public AuthenticationResult(Exception exception, string provider) + : this(isSuccessful: false) + { + if (exception == null) + { throw new ArgumentNullException("exception"); } this.Error = exception; + this.Provider = provider; } /// <summary> diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth/AuthenticationOnlyCookieOAuthTokenManager.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth/AuthenticationOnlyCookieOAuthTokenManager.cs new file mode 100644 index 0000000..10cf39d --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth/AuthenticationOnlyCookieOAuthTokenManager.cs @@ -0,0 +1,96 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthenticationOnlyCookieOAuthTokenManager.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Text; + using System.Web; + using System.Web.Security; + + /// <summary> + /// Stores OAuth tokens in the current request's cookie + /// </summary> + public class AuthenticationOnlyCookieOAuthTokenManager : IOAuthTokenManager { + /// <summary> + /// Key used for token cookie + /// </summary> + private const string TokenCookieKey = "OAuthTokenSecret"; + + /// <summary> + /// Primary request context. + /// </summary> + private readonly HttpContextBase primaryContext; + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationOnlyCookieOAuthTokenManager"/> class. + /// </summary> + public AuthenticationOnlyCookieOAuthTokenManager() { + } + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationOnlyCookieOAuthTokenManager"/> class. + /// </summary> + /// <param name="context">The current request context.</param> + public AuthenticationOnlyCookieOAuthTokenManager(HttpContextBase context) { + this.primaryContext = context; + } + + /// <summary> + /// Gets the effective HttpContext object to use. + /// </summary> + private HttpContextBase Context { + get { + return this.primaryContext ?? new HttpContextWrapper(HttpContext.Current); + } + } + + /// <summary> + /// Gets the token secret from the specified token. + /// </summary> + /// <param name="token">The token.</param> + /// <returns> + /// The token's secret + /// </returns> + public 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); + 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 void ReplaceRequestTokenWithAccessToken(string requestToken, string accessToken, string accessTokenSecret) { + var cookie = new HttpCookie(TokenCookieKey) { + Value = string.Empty, + Expires = DateTime.UtcNow.AddDays(-5) + }; + this.Context.Response.Cookies.Set(cookie); + } + + /// <summary> + /// Stores the request token together with its secret. + /// </summary> + /// <param name="requestToken">The request token.</param> + /// <param name="requestTokenSecret">The request token secret.</param> + public void StoreRequestToken(string requestToken, string requestTokenSecret) { + var cookie = new HttpCookie(TokenCookieKey); + byte[] cookieBytes = Encoding.UTF8.GetBytes(requestTokenSecret); + var secretBytes = MachineKeyUtil.Protect(cookieBytes, TokenCookieKey, "Token:" + requestToken); + cookie.Values[requestToken] = HttpServerUtility.UrlTokenEncode(secretBytes); + this.Context.Response.Cookies.Set(cookie); + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth/LinkedInClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth/LinkedInClient.cs index d349576..ac8186d 100644 --- a/src/DotNetOpenAuth.AspNet/Clients/OAuth/LinkedInClient.cs +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth/LinkedInClient.cs @@ -48,6 +48,9 @@ namespace DotNetOpenAuth.AspNet.Clients { /// <summary> /// Initializes a new instance of the <see cref="LinkedInClient"/> class. /// </summary> + /// <remarks> + /// Tokens exchanged during the OAuth handshake are stored in cookies. + /// </remarks> /// <param name="consumerKey"> /// The LinkedIn app's consumer key. /// </param> @@ -57,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) - : base("linkedIn", LinkedInServiceDescription, consumerKey, consumerSecret) { } + : this(consumerKey, consumerSecret, new AuthenticationOnlyCookieOAuthTokenManager()) { } /// <summary> /// Initializes a new instance of the <see cref="LinkedInClient"/> class. diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth/TwitterClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth/TwitterClient.cs index 0ec0780..96c1701 100644 --- a/src/DotNetOpenAuth.AspNet/Clients/OAuth/TwitterClient.cs +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth/TwitterClient.cs @@ -28,15 +28,15 @@ namespace DotNetOpenAuth.AspNet.Clients { public static readonly ServiceProviderDescription TwitterServiceDescription = new ServiceProviderDescription { RequestTokenEndpoint = new MessageReceivingEndpoint( - "https://twitter.com/oauth/request_token", + "https://api.twitter.com/oauth/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), UserAuthorizationEndpoint = new MessageReceivingEndpoint( - "https://twitter.com/oauth/authenticate", + "https://api.twitter.com/oauth/authenticate", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), AccessTokenEndpoint = new MessageReceivingEndpoint( - "https://twitter.com/oauth/access_token", + "https://api.twitter.com/oauth/access_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() }, }; @@ -48,6 +48,9 @@ namespace DotNetOpenAuth.AspNet.Clients { /// <summary> /// Initializes a new instance of the <see cref="TwitterClient"/> class with the specified consumer key and consumer secret. /// </summary> + /// <remarks> + /// Tokens exchanged during the OAuth handshake are stored in cookies. + /// </remarks> /// <param name="consumerKey"> /// The consumer key. /// </param> @@ -57,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 TwitterClient(string consumerKey, string consumerSecret) - : base("twitter", TwitterServiceDescription, consumerKey, consumerSecret) { } + : this(consumerKey, consumerSecret, new AuthenticationOnlyCookieOAuthTokenManager()) { } /// <summary> /// Initializes a new instance of the <see cref="TwitterClient"/> class. diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookClient.cs index f4ad20b..8cb5cc5 100644 --- a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookClient.cs +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookClient.cs @@ -76,7 +76,10 @@ namespace DotNetOpenAuth.AspNet.Clients { // Note: Facebook doesn't like us to url-encode the redirect_uri value var builder = new UriBuilder(AuthorizationEndpoint); builder.AppendQueryArgs( - new Dictionary<string, string> { { "client_id", this.appId }, { "redirect_uri", returnUrl.AbsoluteUri }, }); + new Dictionary<string, string> { + { "client_id", this.appId }, + { "redirect_uri", returnUrl.AbsoluteUri } + }); return builder.Uri; } @@ -127,7 +130,7 @@ namespace DotNetOpenAuth.AspNet.Clients { builder.AppendQueryArgs( new Dictionary<string, string> { { "client_id", this.appId }, - { "redirect_uri", returnUrl.AbsoluteUri }, + { "redirect_uri", NormalizeHexEncoding(returnUrl.AbsoluteUri) }, { "client_secret", this.appSecret }, { "code", authorizationCode }, }); @@ -143,6 +146,28 @@ namespace DotNetOpenAuth.AspNet.Clients { } } + /// <summary> + /// Converts any % encoded values in the URL to uppercase. + /// </summary> + /// <param name="url">The URL string to normalize</param> + /// <returns>The normalized url</returns> + /// <example>NormalizeHexEncoding("Login.aspx?ReturnUrl=%2fAccount%2fManage.aspx") returns "Login.aspx?ReturnUrl=%2FAccount%2FManage.aspx"</example> + /// <remarks> + /// There is an issue in Facebook whereby it will rejects the redirect_uri value if + /// the url contains lowercase % encoded values. + /// </remarks> + private static string NormalizeHexEncoding(string url) { + var chars = url.ToCharArray(); + for (int i = 0; i < chars.Length - 2; i++) { + if (chars[i] == '%') { + chars[i + 1] = char.ToUpperInvariant(chars[i + 1]); + chars[i + 2] = char.ToUpperInvariant(chars[i + 2]); + i += 2; + } + } + return new string(chars); + } + #endregion } -} +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/OAuth2Client.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/OAuth2Client.cs index cac4261..138fac2 100644 --- a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/OAuth2Client.cs +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/OAuth2Client.cs @@ -21,11 +21,6 @@ namespace DotNetOpenAuth.AspNet.Clients { /// </summary> private readonly string providerName; - /// <summary> - /// The return url. - /// </summary> - private Uri returnUrl; - #endregion #region Constructors and Destructors @@ -71,8 +66,6 @@ namespace DotNetOpenAuth.AspNet.Clients { Requires.NotNull(context, "context"); Requires.NotNull(returnUrl, "returnUrl"); - this.returnUrl = returnUrl; - string redirectUrl = this.GetServiceLoginUrl(returnUrl).AbsoluteUri; context.Response.Redirect(redirectUrl, endResponse: true); } @@ -87,8 +80,7 @@ namespace DotNetOpenAuth.AspNet.Clients { /// An instance of <see cref="AuthenticationResult"/> containing authentication result. /// </returns> public AuthenticationResult VerifyAuthentication(HttpContextBase context) { - Requires.NotNull(this.returnUrl, "this.returnUrl"); - return VerifyAuthentication(context, this.returnUrl); + throw new InvalidOperationException(WebResources.OAuthRequireReturnUrl); } /// <summary> diff --git a/src/DotNetOpenAuth.AspNet/Clients/OpenID/GoogleOpenIdClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OpenID/GoogleOpenIdClient.cs index aedcb80..6b4061a 100644 --- a/src/DotNetOpenAuth.AspNet/Clients/OpenID/GoogleOpenIdClient.cs +++ b/src/DotNetOpenAuth.AspNet/Clients/OpenID/GoogleOpenIdClient.cs @@ -37,8 +37,7 @@ namespace DotNetOpenAuth.AspNet.Clients { if (fetchResponse != null) { var extraData = new Dictionary<string, string>(); extraData.AddItemIfNotEmpty("email", fetchResponse.GetAttributeValue(WellKnownAttributes.Contact.Email)); - extraData.AddItemIfNotEmpty( - "country", fetchResponse.GetAttributeValue(WellKnownAttributes.Contact.HomeAddress.Country)); + extraData.AddItemIfNotEmpty("country", fetchResponse.GetAttributeValue(WellKnownAttributes.Contact.HomeAddress.Country)); extraData.AddItemIfNotEmpty("firstName", fetchResponse.GetAttributeValue(WellKnownAttributes.Name.First)); extraData.AddItemIfNotEmpty("lastName", fetchResponse.GetAttributeValue(WellKnownAttributes.Name.Last)); @@ -67,4 +66,4 @@ namespace DotNetOpenAuth.AspNet.Clients { #endregion } -} +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj b/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj index f1fbacd..5a005e2 100644 --- a/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj +++ b/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj @@ -42,6 +42,9 @@ <ItemGroup> <Compile Include="AuthenticationResult.cs" /> <Compile Include="Clients\DictionaryExtensions.cs" /> + <Compile Include="Clients\OAuth\AuthenticationOnlyCookieOAuthTokenManager.cs"> + <SubType>Code</SubType> + </Compile> <Compile Include="Clients\OAuth\IOAuthTokenManager.cs" /> <Compile Include="Clients\OAuth\SimpleConsumerTokenManager.cs" /> <Compile Include="IAuthenticationClient.cs" /> @@ -61,6 +64,7 @@ <Compile Include="Clients\OpenID\GoogleOpenIdClient.cs" /> <Compile Include="Clients\OpenID\OpenIdClient.cs" /> <Compile Include="Clients\OpenID\YahooOpenIdClient.cs" /> + <Compile Include="MachineKeyUtil.cs" /> <Compile Include="UriHelper.cs" /> <Compile Include="IOpenAuthDataProvider.cs" /> <Compile Include="OpenAuthAuthenticationTicketHelper.cs" /> diff --git a/src/DotNetOpenAuth.AspNet/MachineKeyUtil.cs b/src/DotNetOpenAuth.AspNet/MachineKeyUtil.cs new file mode 100644 index 0000000..ef49652 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/MachineKeyUtil.cs @@ -0,0 +1,344 @@ +//----------------------------------------------------------------------- +// <copyright file="MachineKeyUtil.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Net; + using System.Security.Cryptography; + using System.Text; + using System.Web.Security; + + /// <summary> + /// Provides helpers that mimic the ASP.NET 4.5 MachineKey.Protect / Unprotect APIs, + /// even when running on ASP.NET 4.0. Consumers are expected to follow the same + /// conventions used by the MachineKey.Protect / Unprotect APIs (consult MSDN docs + /// for how these are meant to be used). Additionally, since this helper class + /// dynamically switches between the two based on whether the current application is + /// .NET 4.0 or 4.5, consumers should never persist output from the Protect method + /// since the implementation will change when upgrading 4.0 -> 4.5. This should be + /// used for transient data only. + /// </summary> + public static class MachineKeyUtil { + /// <summary> + /// MachineKey implementation depending on the target .NET framework version + /// </summary> + private static readonly IMachineKey MachineKeyImpl = GetMachineKeyImpl(); + + /// <summary> + /// ProtectUnprotect delegate. + /// </summary> + /// <param name="data">The data.</param> + /// <param name="purposes">The purposes.</param> + /// <returns>Result of either Protect or Unprotect methods.</returns> + private delegate byte[] ProtectUnprotect(byte[] data, string[] purposes); + + /// <summary> + /// Abstract the MachineKey implementation in .NET 4.0 and 4.5 + /// </summary> + private interface IMachineKey { + /// <summary> + /// Protects the specified user data. + /// </summary> + /// <param name="userData">The user data.</param> + /// <param name="purposes">The purposes.</param> + /// <returns>The protected data.</returns> + byte[] Protect(byte[] userData, string[] purposes); + + /// <summary> + /// Unprotects the specified protected data. + /// </summary> + /// <param name="protectedData">The protected data.</param> + /// <param name="purposes">The purposes.</param> + /// <returns>The unprotected data.</returns> + byte[] Unprotect(byte[] protectedData, string[] purposes); + } + + /// <summary> + /// Protects the specified user data. + /// </summary> + /// <param name="userData">The user data.</param> + /// <param name="purposes">The purposes.</param> + /// <returns>The encrypted data</returns> + public static byte[] Protect(byte[] userData, params string[] purposes) { + return MachineKeyImpl.Protect(userData, purposes); + } + + /// <summary> + /// Unprotects the specified protected data. + /// </summary> + /// <param name="protectedData">The protected data.</param> + /// <param name="purposes">The purposes.</param> + /// <returns>The unencrypted data</returns> + public static byte[] Unprotect(byte[] protectedData, params string[] purposes) { + return MachineKeyImpl.Unprotect(protectedData, purposes); + } + + /// <summary> + /// Gets the machine key implementation based on the runtime framework version. + /// </summary> + /// <returns>The machine key implementation</returns> + private static IMachineKey GetMachineKeyImpl() { + ProtectUnprotect protectThunk = (ProtectUnprotect)Delegate.CreateDelegate(typeof(ProtectUnprotect), typeof(MachineKey), "Protect", ignoreCase: false, throwOnBindFailure: false); + ProtectUnprotect unprotectThunk = (ProtectUnprotect)Delegate.CreateDelegate(typeof(ProtectUnprotect), typeof(MachineKey), "Unprotect", ignoreCase: false, throwOnBindFailure: false); + + return (protectThunk != null && unprotectThunk != null) + ? (IMachineKey)new MachineKey45(protectThunk, unprotectThunk) // ASP.NET 4.5 + : (IMachineKey)new MachineKey40(); // ASP.NET 4.0 + } + + /// <summary> + /// On ASP.NET 4.0, we perform some transforms which mimic the behaviors of MachineKey.Protect + /// and Unprotect. + /// </summary> + private sealed class MachineKey40 : IMachineKey { + /// <summary> + /// This is the magic header that identifies a MachineKey40 payload. + /// It helps differentiate this from other encrypted payloads.</summary> + private const uint MagicHeader = 0x8519140c; + + /// <summary> + /// The SHA-256 factory to be used. + /// </summary> + private static readonly Func<SHA256> sha256Factory = GetSHA256Factory(); + + /// <summary> + /// Protects the specified user data. + /// </summary> + /// <param name="userData">The user data.</param> + /// <param name="purposes">The purposes.</param> + /// <returns>The protected data</returns> + public byte[] Protect(byte[] userData, string[] purposes) { + if (userData == null) { + throw new ArgumentNullException("userData"); + } + + // dataWithHeader = {magic header} .. {purposes} .. {userData} + byte[] dataWithHeader = new byte[checked(4 /* magic header */ + (256 / 8) /* purposes */ + userData.Length)]; + unchecked { + dataWithHeader[0] = (byte)(MagicHeader >> 24); + dataWithHeader[1] = (byte)(MagicHeader >> 16); + dataWithHeader[2] = (byte)(MagicHeader >> 8); + dataWithHeader[3] = (byte)MagicHeader; + } + byte[] purposeHash = ComputeSHA256(purposes); + Buffer.BlockCopy(purposeHash, 0, dataWithHeader, 4, purposeHash.Length); + Buffer.BlockCopy(userData, 0, dataWithHeader, 4 + (256 / 8), userData.Length); + + // encrypt + sign + string hexValue = MachineKey.Encode(dataWithHeader, MachineKeyProtection.All); + + // convert hex -> binary + byte[] binary = HexToBinary(hexValue); + return binary; + } + + /// <summary> + /// Unprotects the specified protected data. + /// </summary> + /// <param name="protectedData">The protected data.</param> + /// <param name="purposes">The purposes.</param> + /// <returns>The unprotected data</returns> + public byte[] Unprotect(byte[] protectedData, string[] purposes) { + if (protectedData == null) { + throw new ArgumentNullException("protectedData"); + } + + // convert binary -> hex and calculate what the purpose should read + string hexEncodedData = BinaryToHex(protectedData); + byte[] purposeHash = ComputeSHA256(purposes); + + try { + // decrypt / verify signature + byte[] dataWithHeader = MachineKey.Decode(hexEncodedData, MachineKeyProtection.All); + + // validate magic header and purpose string + if (dataWithHeader != null + && dataWithHeader.Length >= (4 + (256 / 8)) + && (uint)IPAddress.NetworkToHostOrder(BitConverter.ToInt32(dataWithHeader, 0)) == MagicHeader + && AreByteArraysEqual(new ArraySegment<byte>(purposeHash), new ArraySegment<byte>(dataWithHeader, 4, 256 / 8))) { + // validation succeeded + byte[] userData = new byte[dataWithHeader.Length - 4 - (256 / 8)]; + Buffer.BlockCopy(dataWithHeader, 4 + (256 / 8), userData, 0, userData.Length); + return userData; + } + } + catch { + // swallow since will be rethrown immediately below + } + + // if we reached this point, some cryptographic operation failed + throw new CryptographicException(WebResources.Generic_CryptoFailure); + } + + /// <summary> + /// Convert bytes to hex string. + /// </summary> + /// <param name="binary">The input array.</param> + /// <returns>Hex string</returns> + internal static string BinaryToHex(byte[] binary) { + StringBuilder builder = new StringBuilder(checked(binary.Length * 2)); + for (int i = 0; i < binary.Length; i++) { + byte b = binary[i]; + builder.Append(HexDigit(b >> 4)); + builder.Append(HexDigit(b & 0x0F)); + } + string result = builder.ToString(); + return result; + } + + /// <summary> + /// This method is specially written to take the same amount of time + /// regardless of where 'a' and 'b' differ. Please do not optimize it.</summary> + /// <param name="a">first array.</param> + /// <param name="b">second array.</param> + /// <returns><c href="true" /> if equal, others <c href="false" /></returns> + private static bool AreByteArraysEqual(ArraySegment<byte> a, ArraySegment<byte> b) { + if (a.Count != b.Count) { + return false; + } + + bool areEqual = true; + for (int i = 0; i < a.Count; i++) { + areEqual &= a.Array[a.Offset + i] == b.Array[b.Offset + i]; + } + return areEqual; + } + + /// <summary> + /// Computes a SHA256 hash over all of the input parameters. + /// Each parameter is UTF8 encoded and preceded by a 7-bit encoded</summary> + /// integer describing the encoded byte length of the string. + /// <param name="parameters">The parameters.</param> + /// <returns>The output hash</returns> + [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "MemoryStream is resilient to double-Dispose")] + private static byte[] ComputeSHA256(IList<string> parameters) { + using (MemoryStream ms = new MemoryStream()) { + using (BinaryWriter bw = new BinaryWriter(ms)) { + if (parameters != null) { + foreach (string parameter in parameters) { + bw.Write(parameter); // also writes the length as a prefix; unambiguous + } + bw.Flush(); + } + + using (SHA256 sha256 = sha256Factory()) { + byte[] retVal = sha256.ComputeHash(ms.GetBuffer(), 0, checked((int)ms.Length)); + return retVal; + } + } + } + } + + /// <summary> + /// Gets the SHA-256 factory. + /// </summary> + /// <returns>SHA256 factory</returns> + private static Func<SHA256> GetSHA256Factory() { + // Note: ASP.NET 4.5 always prefers CNG, but the CNG algorithms are not that + // performant on 4.0 and below. The following list is optimized for speed + // given our scenarios. + if (!CryptoConfig.AllowOnlyFipsAlgorithms) { + // This provider is not FIPS-compliant, so we can't use it if FIPS compliance + // is mandatory. + return () => new SHA256Managed(); + } + + try { + using (SHA256Cng sha256 = new SHA256Cng()) { + return () => new SHA256Cng(); + } + } + catch (PlatformNotSupportedException) { + // CNG not supported (perhaps because we're not on Windows Vista or above); move on + } + + // If all else fails, fall back to CAPI. + return () => new SHA256CryptoServiceProvider(); + } + + /// <summary> + /// Convert to hex character + /// </summary> + /// <param name="value">The value to be converted.</param> + /// <returns>Hex character</returns> + private static char HexDigit(int value) { + return (char)(value > 9 ? value + '7' : value + '0'); + } + + /// <summary> + /// Convert hdex string to bytes. + /// </summary> + /// <param name="hex">Input hex string.</param> + /// <returns>The bytes</returns> + private static byte[] HexToBinary(string hex) { + int size = hex.Length / 2; + byte[] bytes = new byte[size]; + for (int idx = 0; idx < size; idx++) { + bytes[idx] = (byte)((HexValue(hex[idx * 2]) << 4) + HexValue(hex[(idx * 2) + 1])); + } + return bytes; + } + + /// <summary> + /// Convert hex digit to byte. + /// </summary> + /// <param name="digit">The hex digit.</param> + /// <returns>The byte</returns> + private static int HexValue(char digit) { + return digit > '9' ? digit - '7' : digit - '0'; + } + } + + /// <summary> + /// On ASP.NET 4.5, we can just delegate to MachineKey.Protect and MachineKey.Unprotect directly, + /// which contain optimized code paths. + /// </summary> + private sealed class MachineKey45 : IMachineKey { + /// <summary> + /// Protect thunk + /// </summary> + private readonly ProtectUnprotect protectThunk; + + /// <summary> + /// Unprotect thunk + /// </summary> + private readonly ProtectUnprotect unprotectThunk; + + /// <summary> + /// Initializes a new instance of the <see cref="MachineKey45"/> class. + /// </summary> + /// <param name="protectThunk">The protect thunk.</param> + /// <param name="unprotectThunk">The unprotect thunk.</param> + public MachineKey45(ProtectUnprotect protectThunk, ProtectUnprotect unprotectThunk) { + this.protectThunk = protectThunk; + this.unprotectThunk = unprotectThunk; + } + + /// <summary> + /// Protects the specified user data. + /// </summary> + /// <param name="userData">The user data.</param> + /// <param name="purposes">The purposes.</param> + /// <returns>The protected data</returns> + public byte[] Protect(byte[] userData, string[] purposes) { + return this.protectThunk(userData, purposes); + } + + /// <summary> + /// Unprotects the specified protected data. + /// </summary> + /// <param name="protectedData">The protected data.</param> + /// <param name="purposes">The purposes.</param> + /// <returns>The unprotected data</returns> + public byte[] Unprotect(byte[] protectedData, string[] purposes) { + return this.unprotectThunk(protectedData, purposes); + } + } + } +} diff --git a/src/DotNetOpenAuth.AspNet/OpenAuthAuthenticationTicketHelper.cs b/src/DotNetOpenAuth.AspNet/OpenAuthAuthenticationTicketHelper.cs index 3fc3a21..f51de1c 100644 --- a/src/DotNetOpenAuth.AspNet/OpenAuthAuthenticationTicketHelper.cs +++ b/src/DotNetOpenAuth.AspNet/OpenAuthAuthenticationTicketHelper.cs @@ -106,10 +106,16 @@ namespace DotNetOpenAuth.AspNet { var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket) { HttpOnly = true, - Path = FormsAuthentication.FormsCookiePath, - Secure = FormsAuthentication.RequireSSL + Path = FormsAuthentication.FormsCookiePath }; + // only set Secure if FormsAuthentication requires SSL. + // otherwise, leave it to default value + if (FormsAuthentication.RequireSSL) + { + cookie.Secure = true; + } + if (FormsAuthentication.CookieDomain != null) { cookie.Domain = FormsAuthentication.CookieDomain; } @@ -123,4 +129,4 @@ namespace DotNetOpenAuth.AspNet { #endregion } -} +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.AspNet/OpenAuthSecurityManager.cs b/src/DotNetOpenAuth.AspNet/OpenAuthSecurityManager.cs index ea2ba54..32e6b04 100644 --- a/src/DotNetOpenAuth.AspNet/OpenAuthSecurityManager.cs +++ b/src/DotNetOpenAuth.AspNet/OpenAuthSecurityManager.cs @@ -140,7 +140,8 @@ namespace DotNetOpenAuth.AspNet { Uri uri; if (!string.IsNullOrEmpty(returnUrl)) { uri = UriHelper.ConvertToAbsoluteUri(returnUrl, this.requestContext); - } else { + } + else { uri = this.requestContext.Request.GetPublicFacingUrl(); } @@ -155,24 +156,16 @@ namespace DotNetOpenAuth.AspNet { /// </summary> /// <returns>The result of the authentication.</returns> public AuthenticationResult VerifyAuthentication() { - AuthenticationResult result = this.authenticationProvider.VerifyAuthentication(this.requestContext); - if (!result.IsSuccessful) { - // if the result is a Failed result, creates a new Failed response which has providerName info. - result = new AuthenticationResult( - isSuccessful: false, - provider: this.authenticationProvider.ProviderName, - providerUserId: null, - userName: null, - extraData: null); - } - - return result; + return this.VerifyAuthenticationCore(() => this.authenticationProvider.VerifyAuthentication(this.requestContext)); } /// <summary> /// Checks if user is successfully authenticated when user is redirected back to this user. /// </summary> /// <param name="returnUrl">The return Url which must match exactly the Url passed into RequestAuthentication() earlier.</param> + /// <remarks> + /// This method only applies to OAuth2 providers. For other providers, it ignores the returnUrl parameter. + /// </remarks> /// <returns> /// The result of the authentication. /// </returns> @@ -195,7 +188,21 @@ namespace DotNetOpenAuth.AspNet { // the login when user is redirected back to this page uri = uri.AttachQueryStringParameter(ProviderQueryStringName, this.authenticationProvider.ProviderName); - AuthenticationResult result = oauth2Client.VerifyAuthentication(this.requestContext, uri); + return this.VerifyAuthenticationCore(() => oauth2Client.VerifyAuthentication(this.requestContext, uri)); + } + else { + return this.VerifyAuthentication(); + } + } + + /// <summary> + /// Helper to verify authentiation. + /// </summary> + /// <param name="verifyAuthenticationCall">The real authentication action.</param> + /// <returns>Authentication result</returns> + private AuthenticationResult VerifyAuthenticationCore(Func<AuthenticationResult> verifyAuthenticationCall) { + try { + AuthenticationResult result = verifyAuthenticationCall(); if (!result.IsSuccessful) { // if the result is a Failed result, creates a new Failed response which has providerName info. result = new AuthenticationResult( @@ -208,8 +215,8 @@ namespace DotNetOpenAuth.AspNet { return result; } - else { - return this.VerifyAuthentication(); + catch (HttpException exception) { + return new AuthenticationResult(exception.GetBaseException(), this.authenticationProvider.ProviderName); } } diff --git a/src/DotNetOpenAuth.AspNet/WebResources.Designer.cs b/src/DotNetOpenAuth.AspNet/WebResources.Designer.cs index 23a51be..ac49678 100644 --- a/src/DotNetOpenAuth.AspNet/WebResources.Designer.cs +++ b/src/DotNetOpenAuth.AspNet/WebResources.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:4.0.30319.261 +// Runtime Version:4.0.30319.488 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -79,6 +79,15 @@ namespace DotNetOpenAuth.AspNet { } /// <summary> + /// Looks up a localized string similar to The provided data could not be decrypted. If the current application is deployed in a web farm configuration, ensure that the 'decryptionKey' and 'validationKey' attributes are explicitly specified in the <machineKey> configuration section.. + /// </summary> + internal static string Generic_CryptoFailure { + get { + return ResourceManager.GetString("Generic_CryptoFailure", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to An OAuth data provider has already been registered for this application.. /// </summary> internal static string OAuthDataProviderRegistered { @@ -88,6 +97,15 @@ namespace DotNetOpenAuth.AspNet { } /// <summary> + /// Looks up a localized string similar to This operation is not supported on the current provider.. + /// </summary> + internal static string OAuthRequireReturnUrl { + get { + return ResourceManager.GetString("OAuthRequireReturnUrl", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Failed to obtain the authentication response from service provider.. /// </summary> internal static string OpenIDFailedToGetResponse { diff --git a/src/DotNetOpenAuth.AspNet/WebResources.resx b/src/DotNetOpenAuth.AspNet/WebResources.resx index 321c097..2ed19df 100644 --- a/src/DotNetOpenAuth.AspNet/WebResources.resx +++ b/src/DotNetOpenAuth.AspNet/WebResources.resx @@ -123,9 +123,15 @@ <data name="FailedToEncryptTicket" xml:space="preserve"> <value>Unable to encrypt the authentication ticket.</value> </data> + <data name="Generic_CryptoFailure" xml:space="preserve"> + <value>The provided data could not be decrypted. If the current application is deployed in a web farm configuration, ensure that the 'decryptionKey' and 'validationKey' attributes are explicitly specified in the <machineKey> configuration section.</value> + </data> <data name="OAuthDataProviderRegistered" xml:space="preserve"> <value>An OAuth data provider has already been registered for this application.</value> </data> + <data name="OAuthRequireReturnUrl" xml:space="preserve"> + <value>This operation is not supported on the current provider.</value> + </data> <data name="OpenIDFailedToGetResponse" xml:space="preserve"> <value>Failed to obtain the authentication response from service provider.</value> </data> |