summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.AspNet
diff options
context:
space:
mode:
Diffstat (limited to 'src/DotNetOpenAuth.AspNet')
-rw-r--r--src/DotNetOpenAuth.AspNet/AuthenticationResult.cs16
-rw-r--r--src/DotNetOpenAuth.AspNet/Clients/OAuth/AuthenticationOnlyCookieOAuthTokenManager.cs96
-rw-r--r--src/DotNetOpenAuth.AspNet/Clients/OAuth/LinkedInClient.cs5
-rw-r--r--src/DotNetOpenAuth.AspNet/Clients/OAuth/TwitterClient.cs11
-rw-r--r--src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookClient.cs31
-rw-r--r--src/DotNetOpenAuth.AspNet/Clients/OAuth2/OAuth2Client.cs10
-rw-r--r--src/DotNetOpenAuth.AspNet/Clients/OpenID/GoogleOpenIdClient.cs5
-rw-r--r--src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj4
-rw-r--r--src/DotNetOpenAuth.AspNet/MachineKeyUtil.cs344
-rw-r--r--src/DotNetOpenAuth.AspNet/OpenAuthAuthenticationTicketHelper.cs12
-rw-r--r--src/DotNetOpenAuth.AspNet/OpenAuthSecurityManager.cs39
-rw-r--r--src/DotNetOpenAuth.AspNet/WebResources.Designer.cs20
-rw-r--r--src/DotNetOpenAuth.AspNet/WebResources.resx6
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 &apos;decryptionKey&apos; and &apos;validationKey&apos; attributes are explicitly specified in the &lt;machineKey&gt; 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 &lt;machineKey&gt; 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>