diff options
author | levib <levib@microsoft.com> | 2012-04-07 03:40:59 +0400 |
---|---|---|
committer | levib <levib@microsoft.com> | 2012-04-07 03:42:35 +0400 |
commit | de43b6ad756800fd3b6e984ec65f9f37dbad723f (patch) | |
tree | dcf9b9af761a2ac8924b37084938a1bf79807883 | |
parent | a257938cd04948862e4af29f44aa45ffaea86592 (diff) |
Responding to customer and partner feedback re: the Anti-XSRF helpers.
What's new:
- Programmatic configuration over various Anti-XSRF behaviors:
-> The name of the cookie to use.
-> Whether SSL is required.
-> Ability to provide a nonce or other "custom data".
- The exception message is now a little less cryptic. It tells you exactly what check failed (e.g. the cookie 'foo' was missing, the token was meant for a different user, etc.).
- The system tries to detect if the current identity is degenerate (e.g. authenticated but without a name) and fails safe. The exception message specifies how to resolve the problem. (This check can be suppressed via config if necessary.)
- Ability to get the cookie and form token strings directly if you want more manual control.
- Built-in support for OpenID and Azure ACS (WIF).
- For most consumers, the token size is smaller.
Breaks:
- The salt / domain / path parameters are all obsolete as error. The customer can achieve the same effect by using the <httpCookies> configuration element or calling the AntiForgery.* APIs that are string-based.
- Not compatible with MVC 1 / 2 / 3. However, this system makes it easier to recover gracefully when an old token is submitted.
CR: marcind; bradwils
SR: naziml
60 files changed, 4282 insertions, 904 deletions
diff --git a/src/System.Web.Mvc/HtmlHelper.cs b/src/System.Web.Mvc/HtmlHelper.cs index d9a5d2d0..43c483bf 100644 --- a/src/System.Web.Mvc/HtmlHelper.cs +++ b/src/System.Web.Mvc/HtmlHelper.cs @@ -106,19 +106,44 @@ namespace System.Web.Mvc return result; } + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")] public MvcHtmlString AntiForgeryToken() { - return AntiForgeryToken(salt: null); + return new MvcHtmlString(AntiForgery.GetHtml().ToString()); } + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "AdditionalDataProvider", Justification = "API name.")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "AntiForgeryConfig", Justification = "API name.")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "AntiForgeryToken", Justification = "API name.")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "httpCookies", Justification = "API name.")] + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Method is obsolete.")] + [Obsolete("This method is deprecated. Use the AntiForgeryToken() method instead. To specify custom data to be embedded within the token, use the static AntiForgeryConfig.AdditionalDataProvider property.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] public MvcHtmlString AntiForgeryToken(string salt) { - return AntiForgeryToken(salt, domain: null, path: null); + if (!String.IsNullOrEmpty(salt)) + { + throw new NotSupportedException("This method is deprecated. Use the AntiForgeryToken() method instead. To specify custom data to be embedded within the token, use the static AntiForgeryConfig.AdditionalDataProvider property."); + } + + return AntiForgeryToken(); } + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "AdditionalDataProvider", Justification = "API name.")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "AntiForgeryConfig", Justification = "API name.")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "AntiForgeryToken", Justification = "API name.")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "httpCookies", Justification = "API name.")] + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Method is obsolete.")] + [Obsolete("This method is deprecated. Use the AntiForgeryToken() method instead. To specify a custom domain for the generated cookie, use the <httpCookies> configuration element. To specify custom data to be embedded within the token, use the static AntiForgeryConfig.AdditionalDataProvider property.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] public MvcHtmlString AntiForgeryToken(string salt, string domain, string path) { - return new MvcHtmlString(AntiForgery.GetHtml(ViewContext.HttpContext, salt, domain, path).ToString()); + if (!String.IsNullOrEmpty(salt) || !String.IsNullOrEmpty(domain) || !String.IsNullOrEmpty(path)) + { + throw new NotSupportedException("This method is deprecated. Use the AntiForgeryToken() method instead. To specify a custom domain for the generated cookie, use the <httpCookies> configuration element. To specify custom data to be embedded within the token, use the static AntiForgeryConfig.AdditionalDataProvider property."); + } + + return AntiForgeryToken(); } [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "For consistency, all helpers are instance methods.")] diff --git a/src/System.Web.Mvc/ValidateAntiForgeryTokenAttribute.cs b/src/System.Web.Mvc/ValidateAntiForgeryTokenAttribute.cs index b948a0b0..810344c0 100644 --- a/src/System.Web.Mvc/ValidateAntiForgeryTokenAttribute.cs +++ b/src/System.Web.Mvc/ValidateAntiForgeryTokenAttribute.cs @@ -1,4 +1,6 @@ -using System.Diagnostics; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Web.Helpers; namespace System.Web.Mvc @@ -13,19 +15,30 @@ namespace System.Web.Mvc { } - internal ValidateAntiForgeryTokenAttribute(Action<HttpContextBase, string> validateAction) + internal ValidateAntiForgeryTokenAttribute(Action validateAction) { Debug.Assert(validateAction != null); ValidateAction = validateAction; } + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "AdditionalDataProvider", Justification = "API name.")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "AntiForgeryConfig", Justification = "API name.")] + [Obsolete("The 'Salt' property is deprecated. To specify custom data to be embedded within the token, use the static AntiForgeryConfig.AdditionalDataProvider property.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] public string Salt { - get { return _salt ?? String.Empty; } - set { _salt = value; } + get { return _salt; } + set + { + if (!String.IsNullOrEmpty(value)) + { + throw new NotSupportedException("The 'Salt' property is deprecated. To specify custom data to be embedded within the token, use the static AntiForgeryConfig.AdditionalDataProvider property."); + } + _salt = value; + } } - internal Action<HttpContextBase, string> ValidateAction { get; private set; } + internal Action ValidateAction { get; private set; } public void OnAuthorization(AuthorizationContext filterContext) { @@ -34,7 +47,7 @@ namespace System.Web.Mvc throw new ArgumentNullException("filterContext"); } - ValidateAction(filterContext.HttpContext, Salt); + ValidateAction(); } } } diff --git a/src/System.Web.WebPages/GlobalSuppressions.cs b/src/System.Web.WebPages/GlobalSuppressions.cs index 07747b70..6f1e5832 100644 --- a/src/System.Web.WebPages/GlobalSuppressions.cs +++ b/src/System.Web.WebPages/GlobalSuppressions.cs @@ -13,3 +13,7 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.WebPages.Html", Justification = "The namespace contains types specific to Razor. It allows a way for MVC Razor host to identify and remove the namespace")] [assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Mvc", Justification = "This namespace contains TagBuilder and other types forwarded from System.Web.Mvc. The namespace must stay the way it is for type forwarding to work")] [assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.WebPages.Instrumentation", Justification = "This namespace contains Instrumentation types and represents an isolated set of functionality.")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "accesscontrolservice", Scope = "resource", Target = "System.Web.WebPages.Resources.WebPageResources.resources", Justification = "This is part of a URL.")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "identityprovider", Scope = "resource", Target = "System.Web.WebPages.Resources.WebPageResources.resources", Justification = "This is part of a URL.")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "nameidentifier", Scope = "resource", Target = "System.Web.WebPages.Resources.WebPageResources.resources", Justification = "This is part of a URL.")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "xmlsoap", Scope = "resource", Target = "System.Web.WebPages.Resources.WebPageResources.resources", Justification = "This is part of a URL.")] diff --git a/src/System.Web.WebPages/Helpers/AntiForgery.cs b/src/System.Web.WebPages/Helpers/AntiForgery.cs index afd827f9..4bfac015 100644 --- a/src/System.Web.WebPages/Helpers/AntiForgery.cs +++ b/src/System.Web.WebPages/Helpers/AntiForgery.cs @@ -1,11 +1,28 @@ -using System.Web.WebPages.Resources; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Web.Helpers.AntiXsrf; +using System.Web.Mvc; +using System.Web.WebPages.Resources; namespace System.Web.Helpers { + /// <summary> + /// Provides access to the anti-forgery system, which provides protection against + /// Cross-site Request Forgery (XSRF, also called CSRF) attacks. + /// </summary> public static class AntiForgery { private static readonly AntiForgeryWorker _worker = new AntiForgeryWorker(); + /// <summary> + /// Generates an anti-forgery token for this request. This token can + /// be validated by calling the Validate() method. + /// </summary> + /// <returns>An HTML string corresponding to an <input type="hidden"> + /// element. This element should be put inside a <form>.</returns> + /// <remarks> + /// This method has a side effect: it may set a response cookie. + /// </remarks> public static HtmlString GetHtml() { if (HttpContext.Current == null) @@ -13,9 +30,50 @@ namespace System.Web.Helpers throw new ArgumentException(WebPageResources.HttpContextUnavailable); } - return GetHtml(new HttpContextWrapper(HttpContext.Current), salt: null, domain: null, path: null); + TagBuilder retVal = _worker.GetFormInputElement(new HttpContextWrapper(HttpContext.Current)); + return retVal.ToHtmlString(TagRenderMode.SelfClosing); } + /// <summary> + /// Generates an anti-forgery token pair (cookie and form token) for this request. + /// This method is similar to GetHtml(), but this method gives the caller control + /// over how to persist the returned values. To validate these tokens, call the + /// appropriate overload of Validate. + /// </summary> + /// <param name="oldCookieToken">The anti-forgery token - if any - that already existed + /// for this request. May be null. The anti-forgery system will try to reuse this cookie + /// value when generating a matching form token.</param> + /// <param name="newCookieToken">Will contain a new cookie value if the old cookie token + /// was null or invalid. If this value is non-null when the method completes, the caller + /// must persist this value in the form of a response cookie, and the existing cookie value + /// should be discarded. If this value is null when the method completes, the existing + /// cookie value was valid and needn't be modified.</param> + /// <param name="formToken">The value that should be stored in the <form>. The caller + /// should take care not to accidentally swap the cookie and form tokens.</param> + /// <remarks> + /// Unlike the GetHtml() method, this method has no side effect. The caller + /// is responsible for setting the response cookie and injecting the returned + /// form token as appropriate. + /// </remarks> + [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Justification = "Method is intended for advanced audiences.")] + [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "Method is intended for advanced audiences.")] + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static void GetTokens(string oldCookieToken, out string newCookieToken, out string formToken) + { + if (HttpContext.Current == null) + { + throw new ArgumentException(WebPageResources.HttpContextUnavailable); + } + + _worker.GetTokens(new HttpContextWrapper(HttpContext.Current), oldCookieToken, out newCookieToken, out formToken); + } + + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "AdditionalDataProvider", Justification = "API name.")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "AntiForgeryConfig", Justification = "API name.")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "GetHtml", Justification = "API name.")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "httpCookies", Justification = "API name.")] + [Obsolete("This method is deprecated. Use the GetHtml() method instead. To specify a custom domain for the generated cookie, use the <httpCookies> configuration element. To specify custom data to be embedded within the token, use the static AntiForgeryConfig.AdditionalDataProvider property.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] public static HtmlString GetHtml(HttpContextBase httpContext, string salt, string domain, string path) { if (httpContext == null) @@ -23,18 +81,53 @@ namespace System.Web.Helpers throw new ArgumentNullException("httpContext"); } - return _worker.GetHtml(httpContext, salt, domain, path); + if (!String.IsNullOrEmpty(salt) || !String.IsNullOrEmpty(domain) || !String.IsNullOrEmpty(path)) + { + throw new NotSupportedException("This method is deprecated. Use the GetHtml() method instead. To specify a custom domain for the generated cookie, use the <httpCookies> configuration element. To specify custom data to be embedded within the token, use the static AntiForgeryConfig.AdditionalDataProvider property."); + } + + TagBuilder retVal = _worker.GetFormInputElement(httpContext); + return retVal.ToHtmlString(TagRenderMode.SelfClosing); } + /// <summary> + /// Validates an anti-forgery token that was supplied for this request. + /// The anti-forgery token may be generated by calling GetHtml(). + /// </summary> + /// <remarks> + /// Throws an HttpAntiForgeryException if validation fails. + /// </remarks> public static void Validate() { if (HttpContext.Current == null) { throw new ArgumentException(WebPageResources.HttpContextUnavailable); } - Validate(new HttpContextWrapper(HttpContext.Current), salt: null); + + _worker.Validate(new HttpContextWrapper(HttpContext.Current)); } + /// <summary> + /// Validates an anti-forgery token pair that was generated by the GetTokens method. + /// </summary> + /// <param name="cookieToken">The token that was supplied in the request cookie.</param> + /// <param name="formToken">The token that was supplied in the request form body.</param> + /// <remarks> + /// Throws an HttpAntiForgeryException if validation fails. + /// </remarks> + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static void Validate(string cookieToken, string formToken) + { + if (HttpContext.Current == null) + { + throw new ArgumentException(WebPageResources.HttpContextUnavailable); + } + + _worker.Validate(new HttpContextWrapper(HttpContext.Current), cookieToken, formToken); + } + + [Obsolete("This method is deprecated. Use the Validate() method instead.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] public static void Validate(HttpContextBase httpContext, string salt) { if (httpContext == null) @@ -42,7 +135,12 @@ namespace System.Web.Helpers throw new ArgumentNullException("httpContext"); } - _worker.Validate(httpContext, salt); + if (!String.IsNullOrEmpty(salt)) + { + throw new NotSupportedException("This method is deprecated. Use the Validate() method instead."); + } + + _worker.Validate(httpContext); } } } diff --git a/src/System.Web.WebPages/Helpers/AntiForgeryConfig.cs b/src/System.Web.WebPages/Helpers/AntiForgeryConfig.cs new file mode 100644 index 00000000..fcae9bb4 --- /dev/null +++ b/src/System.Web.WebPages/Helpers/AntiForgeryConfig.cs @@ -0,0 +1,124 @@ +using System.ComponentModel; +using System.Text; + +namespace System.Web.Helpers +{ + /// <summary> + /// Provides programmatic configuration for the anti-forgery token system. + /// </summary> + public static class AntiForgeryConfig + { + internal const string AntiForgeryTokenFieldName = "__RequestVerificationToken"; + + private static string _cookieName; + private static string _uniqueClaimTypeIdentifier; + + /// <summary> + /// Specifies an object that can provide additional data to put into all + /// generated tokens and that can validate additional data in incoming + /// tokens. + /// </summary> + public static IAntiForgeryAdditionalDataProvider AdditionalDataProvider + { + get; + set; + } + + /// <summary> + /// Specifies the name of the cookie that is used by the anti-forgery + /// system. + /// </summary> + /// <remarks> + /// If an explicit name is not provided, the system will automatically + /// generate a name. + /// </remarks> + public static string CookieName + { + get + { + if (_cookieName == null) + { + _cookieName = GetAntiForgeryCookieName(); + } + return _cookieName; + } + set + { + _cookieName = value; + } + } + + /// <summary> + /// Specifies whether SSL is required for the anti-forgery system + /// to operate. If this setting is 'true' and a non-SSL request + /// comes into the system, all anti-forgery APIs will fail. + /// </summary> + public static bool RequireSsl + { + get; + set; + } + + /// <summary> + /// Specifies whether the anti-forgery system should skip checking + /// for conditions that might indicate misuse of the system. Please + /// use caution when setting this switch, as improper use could open + /// security holes in the application. + /// </summary> + /// <remarks> + /// Setting this switch will disable several checks, including: + /// - Identity.IsAuthenticated = true without Identity.Name being set + /// - special-casing claims-based identities + /// </remarks> + [EditorBrowsable(EditorBrowsableState.Never)] + public static bool SuppressIdentityHeuristicChecks + { + get; + set; + } + + /// <summary> + /// If claims-based authorization is in use, specifies the claim + /// type from the identity that is used to uniquely identify the + /// user. If this property is set, all claims-based identities + /// <em>must</em> return unique values for this claim type. + /// </summary> + /// <remarks> + /// If claims-based authorization is in use and this property has + /// not been set, the anti-forgery system will automatically look + /// for claim types "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" + /// and "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider". + /// </remarks> + public static string UniqueClaimTypeIdentifier + { + get + { + return _uniqueClaimTypeIdentifier ?? String.Empty; + } + set + { + _uniqueClaimTypeIdentifier = value; + } + } + + private static string GetAntiForgeryCookieName() + { + return GetAntiForgeryCookieName(HttpRuntime.AppDomainAppVirtualPath); + } + + // If the app path is provided, we're generating a cookie name rather than a field name, and the cookie names should + // be unique so that a development server cookie and an IIS cookie - both running on localhost - don't stomp on + // each other. + internal static string GetAntiForgeryCookieName(string appPath) + { + if (String.IsNullOrEmpty(appPath) || appPath == "/") + { + return AntiForgeryTokenFieldName; + } + else + { + return AntiForgeryTokenFieldName + "_" + HttpServerUtility.UrlTokenEncode(Encoding.UTF8.GetBytes(appPath)); + } + } + } +} diff --git a/src/System.Web.WebPages/Helpers/AntiForgeryData.cs b/src/System.Web.WebPages/Helpers/AntiForgeryData.cs deleted file mode 100644 index 5af09b02..00000000 --- a/src/System.Web.WebPages/Helpers/AntiForgeryData.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Security.Cryptography; -using System.Security.Principal; -using System.Text; - -namespace System.Web.Helpers -{ - internal sealed class AntiForgeryData - { - private const string AntiForgeryTokenFieldName = "__RequestVerificationToken"; - - private const int TokenLength = 128 / 8; - private static readonly RNGCryptoServiceProvider _prng = new RNGCryptoServiceProvider(); - - private DateTime _creationDate = DateTime.UtcNow; - private string _salt; - private string _username; - private string _value; - - public AntiForgeryData() - { - } - - // copy constructor - public AntiForgeryData(AntiForgeryData token) - { - if (token == null) - { - throw new ArgumentNullException("token"); - } - - CreationDate = token.CreationDate; - Salt = token.Salt; - Username = token.Username; - Value = token.Value; - } - - public DateTime CreationDate - { - get { return _creationDate; } - set { _creationDate = value; } - } - - public string Salt - { - get { return _salt ?? String.Empty; } - set { _salt = value; } - } - - public string Username - { - get { return _username ?? String.Empty; } - set { _username = value; } - } - - public string Value - { - get { return _value ?? String.Empty; } - set { _value = value; } - } - - private static string Base64EncodeForCookieName(string s) - { - byte[] rawBytes = Encoding.UTF8.GetBytes(s); - string base64String = Convert.ToBase64String(rawBytes); - - // replace base64-specific characters with characters that are safe for a cookie name - return base64String.Replace('+', '.').Replace('/', '-').Replace('=', '_'); - } - - private static string GenerateRandomTokenString() - { - byte[] tokenBytes = new byte[TokenLength]; - _prng.GetBytes(tokenBytes); - - string token = Convert.ToBase64String(tokenBytes); - return token; - } - - // If the app path is provided, we're generating a cookie name rather than a field name, and the cookie names should - // be unique so that a development server cookie and an IIS cookie - both running on localhost - don't stomp on - // each other. - internal static string GetAntiForgeryTokenName(string appPath) - { - if (String.IsNullOrEmpty(appPath)) - { - return AntiForgeryTokenFieldName; - } - else - { - return AntiForgeryTokenFieldName + "_" + Base64EncodeForCookieName(appPath); - } - } - - internal static string GetUsername(IPrincipal user) - { - if (user != null) - { - IIdentity identity = user.Identity; - if (identity != null && identity.IsAuthenticated) - { - return identity.Name; - } - } - - return String.Empty; - } - - public static AntiForgeryData NewToken() - { - string tokenString = GenerateRandomTokenString(); - return new AntiForgeryData() - { - Value = tokenString - }; - } - } -} diff --git a/src/System.Web.WebPages/Helpers/AntiForgeryDataSerializer.cs b/src/System.Web.WebPages/Helpers/AntiForgeryDataSerializer.cs deleted file mode 100644 index f8df2564..00000000 --- a/src/System.Web.WebPages/Helpers/AntiForgeryDataSerializer.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Text; -using System.Web.Mvc; -using System.Web.Security; -using System.Web.WebPages.Resources; -using Microsoft.Internal.Web.Utils; - -namespace System.Web.Helpers -{ - internal class AntiForgeryDataSerializer - { - // Testing hooks - - internal Func<string, byte[]> Decoder = - (value) => MachineKey.Decode(Base64ToHex(value), MachineKeyProtection.All); - - internal Func<byte[], string> Encoder = - (bytes) => HexToBase64(MachineKey.Encode(bytes, MachineKeyProtection.All).ToUpperInvariant()); - - [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "MemoryStream is resilient to double-Dispose")] - public virtual AntiForgeryData Deserialize(string serializedToken) - { - if (String.IsNullOrEmpty(serializedToken)) - { - throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "serializedToken"); - } - - try - { - using (MemoryStream stream = new MemoryStream(Decoder(serializedToken))) - { - using (BinaryReader reader = new BinaryReader(stream)) - { - return new AntiForgeryData - { - Salt = reader.ReadString(), - Value = reader.ReadString(), - CreationDate = new DateTime(reader.ReadInt64()), - Username = reader.ReadString() - }; - } - } - } - catch - { - throw new HttpAntiForgeryException(WebPageResources.AntiForgeryToken_ValidationFailed); - } - } - - [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "MemoryStream is resilient to double-Dispose")] - public virtual string Serialize(AntiForgeryData token) - { - if (token == null) - { - throw new ArgumentNullException("token"); - } - - using (MemoryStream stream = new MemoryStream()) - { - using (BinaryWriter writer = new BinaryWriter(stream)) - { - writer.Write(token.Salt); - writer.Write(token.Value); - writer.Write(token.CreationDate.Ticks); - writer.Write(token.Username); - - return Encoder(stream.ToArray()); - } - } - } - - // String transformation helpers - - private static string Base64ToHex(string base64) - { - StringBuilder builder = new StringBuilder(base64.Length * 4); - foreach (byte b in Convert.FromBase64String(base64)) - { - builder.Append(HexDigit(b >> 4)); - builder.Append(HexDigit(b & 0x0F)); - } - string result = builder.ToString(); - return result; - } - - internal static char HexDigit(int value) - { - return (char)(value > 9 ? value + '7' : value + '0'); - } - - internal static int HexValue(char digit) - { - return digit > '9' ? digit - '7' : digit - '0'; - } - - private static string HexToBase64(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])); - } - string result = Convert.ToBase64String(bytes); - return result; - } - } -} diff --git a/src/System.Web.WebPages/Helpers/AntiForgeryWorker.cs b/src/System.Web.WebPages/Helpers/AntiForgeryWorker.cs deleted file mode 100644 index 314921b8..00000000 --- a/src/System.Web.WebPages/Helpers/AntiForgeryWorker.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Diagnostics; -using System.Web.Mvc; -using System.Web.WebPages.Resources; - -namespace System.Web.Helpers -{ - internal class AntiForgeryWorker - { - public AntiForgeryWorker() - { - Serializer = new AntiForgeryDataSerializer(); - } - - internal AntiForgeryDataSerializer Serializer { get; set; } - - private static HttpAntiForgeryException CreateValidationException() - { - return new HttpAntiForgeryException(WebPageResources.AntiForgeryToken_ValidationFailed); - } - - public HtmlString GetHtml(HttpContextBase httpContext, string salt, string domain, string path) - { - Debug.Assert(httpContext != null); - - string formValue = GetAntiForgeryTokenAndSetCookie(httpContext, salt, domain, path); - string fieldName = AntiForgeryData.GetAntiForgeryTokenName(null); - - TagBuilder builder = new TagBuilder("input"); - builder.Attributes["type"] = "hidden"; - builder.Attributes["name"] = fieldName; - builder.Attributes["value"] = formValue; - return new HtmlString(builder.ToString(TagRenderMode.SelfClosing)); - } - - private string GetAntiForgeryTokenAndSetCookie(HttpContextBase httpContext, string salt, string domain, string path) - { - string cookieName = AntiForgeryData.GetAntiForgeryTokenName(httpContext.Request.ApplicationPath); - - AntiForgeryData cookieToken = null; - HttpCookie cookie = httpContext.Request.Cookies[cookieName]; - if (cookie != null) - { - try - { - cookieToken = Serializer.Deserialize(cookie.Value); - } - catch (HttpAntiForgeryException) - { - } - } - - if (cookieToken == null) - { - cookieToken = AntiForgeryData.NewToken(); - string cookieValue = Serializer.Serialize(cookieToken); - - HttpCookie newCookie = new HttpCookie(cookieName, cookieValue) { HttpOnly = true, Domain = domain }; - if (!String.IsNullOrEmpty(path)) - { - newCookie.Path = path; - } - httpContext.Response.Cookies.Set(newCookie); - } - - AntiForgeryData formToken = new AntiForgeryData(cookieToken) - { - Salt = salt, - Username = AntiForgeryData.GetUsername(httpContext.User) - }; - return Serializer.Serialize(formToken); - } - - public void Validate(HttpContextBase context, string salt) - { - Debug.Assert(context != null); - - string fieldName = AntiForgeryData.GetAntiForgeryTokenName(null); - string cookieName = AntiForgeryData.GetAntiForgeryTokenName(context.Request.ApplicationPath); - - HttpCookie cookie = context.Request.Cookies[cookieName]; - if (cookie == null || String.IsNullOrEmpty(cookie.Value)) - { - // error: cookie token is missing - throw CreateValidationException(); - } - AntiForgeryData cookieToken = Serializer.Deserialize(cookie.Value); - - string formValue = context.Request.Form[fieldName]; - if (String.IsNullOrEmpty(formValue)) - { - // error: form token is missing - throw CreateValidationException(); - } - AntiForgeryData formToken = Serializer.Deserialize(formValue); - - if (!String.Equals(cookieToken.Value, formToken.Value, StringComparison.Ordinal)) - { - // error: form token does not match cookie token - throw CreateValidationException(); - } - - string currentUsername = AntiForgeryData.GetUsername(context.User); - if (!String.Equals(formToken.Username, currentUsername, StringComparison.OrdinalIgnoreCase)) - { - // error: form token is not valid for this user - // (don't care about cookie token) - throw CreateValidationException(); - } - - if (!String.Equals(salt ?? String.Empty, formToken.Salt, StringComparison.Ordinal)) - { - // error: custom validation failed - throw CreateValidationException(); - } - } - } -} diff --git a/src/System.Web.WebPages/Helpers/AntiXsrf/AntiForgeryConfigWrapper.cs b/src/System.Web.WebPages/Helpers/AntiXsrf/AntiForgeryConfigWrapper.cs new file mode 100644 index 00000000..dd7cfedd --- /dev/null +++ b/src/System.Web.WebPages/Helpers/AntiXsrf/AntiForgeryConfigWrapper.cs @@ -0,0 +1,38 @@ +namespace System.Web.Helpers.AntiXsrf +{ + internal sealed class AntiForgeryConfigWrapper : IAntiForgeryConfig + { + public IAntiForgeryAdditionalDataProvider AdditionalDataProvider + { + get + { + return AntiForgeryConfig.AdditionalDataProvider; + } + } + + public string CookieName + { + get { return AntiForgeryConfig.CookieName; } + } + + public string FormFieldName + { + get { return AntiForgeryConfig.AntiForgeryTokenFieldName; } + } + + public bool RequireSSL + { + get { return AntiForgeryConfig.RequireSsl; } + } + + public bool SuppressIdentityHeuristicChecks + { + get { return AntiForgeryConfig.SuppressIdentityHeuristicChecks; } + } + + public string UniqueClaimTypeIdentifier + { + get { return AntiForgeryConfig.UniqueClaimTypeIdentifier; } + } + } +} diff --git a/src/System.Web.WebPages/Helpers/AntiXsrf/AntiForgeryToken.cs b/src/System.Web.WebPages/Helpers/AntiXsrf/AntiForgeryToken.cs new file mode 100644 index 00000000..dd4b405d --- /dev/null +++ b/src/System.Web.WebPages/Helpers/AntiXsrf/AntiForgeryToken.cs @@ -0,0 +1,58 @@ +namespace System.Web.Helpers.AntiXsrf +{ + // Represents the security token for the Anti-XSRF system. + // The token is a random 128-bit value that correlates the session with the request body. + internal sealed class AntiForgeryToken + { + internal const int SecurityTokenBitLength = 128; + internal const int ClaimUidBitLength = 256; + + private string _additionalData; + private BinaryBlob _securityToken; + private string _username; + + public string AdditionalData + { + get + { + return _additionalData ?? String.Empty; + } + set + { + _additionalData = value; + } + } + + public BinaryBlob ClaimUid { get; set; } + + public bool IsSessionToken { get; set; } + + public BinaryBlob SecurityToken + { + get + { + if (_securityToken == null) + { + _securityToken = new BinaryBlob(SecurityTokenBitLength); + } + return _securityToken; + } + set + { + _securityToken = value; + } + } + + public string Username + { + get + { + return _username ?? String.Empty; + } + set + { + _username = value; + } + } + } +} diff --git a/src/System.Web.WebPages/Helpers/AntiXsrf/AntiForgeryTokenSerializer.cs b/src/System.Web.WebPages/Helpers/AntiXsrf/AntiForgeryTokenSerializer.cs new file mode 100644 index 00000000..ba5443a2 --- /dev/null +++ b/src/System.Web.WebPages/Helpers/AntiXsrf/AntiForgeryTokenSerializer.cs @@ -0,0 +1,138 @@ +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.IO; +using System.Web.Mvc; + +namespace System.Web.Helpers.AntiXsrf +{ + internal sealed class AntiForgeryTokenSerializer : IAntiForgeryTokenSerializer + { + private const byte TokenVersion = 0x01; + private readonly ICryptoSystem _cryptoSystem; + + public AntiForgeryTokenSerializer() + : this(new MachineKeyCryptoSystem()) + { + } + + // for unit testing + internal AntiForgeryTokenSerializer(ICryptoSystem cryptoSystem) + { + _cryptoSystem = cryptoSystem; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Failures are homogenized; caller handles appropriately.")] + [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "MemoryStream is safe for multi-dispose.")] + public AntiForgeryToken Deserialize(string serializedToken) + { + try + { + using (MemoryStream stream = new MemoryStream(_cryptoSystem.Unprotect(serializedToken))) + { + using (BinaryReader reader = new BinaryReader(stream)) + { + AntiForgeryToken token = DeserializeImpl(reader); + if (token != null) + { + return token; + } + } + } + } + catch + { + // swallow all exceptions - homogenize error if something went wrong + } + + // if we reached this point, something went wrong deserializing + throw HttpAntiForgeryException.CreateDeserializationFailedException(); + } + + /* The serialized format of the anti-XSRF token is as follows: + * Version: 1 byte integer + * SecurityToken: 16 byte binary blob + * IsSessionToken: 1 byte Boolean + * [if IsSessionToken = true] + * +- IsClaimsBased: 1 byte Boolean + * | [if IsClaimsBased = true] + * | `- ClaimUid: 32 byte binary blob + * | [if IsClaimsBased = false] + * | `- Username: UTF-8 string with 7-bit integer length prefix + * `- AdditionalData: UTF-8 string with 7-bit integer length prefix + */ + private static AntiForgeryToken DeserializeImpl(BinaryReader reader) + { + // we can only consume tokens of the same serialized version that we generate + byte embeddedVersion = reader.ReadByte(); + if (embeddedVersion != TokenVersion) + { + return null; + } + + AntiForgeryToken deserializedToken = new AntiForgeryToken(); + byte[] securityTokenBytes = reader.ReadBytes(AntiForgeryToken.SecurityTokenBitLength / 8); + deserializedToken.SecurityToken = new BinaryBlob(AntiForgeryToken.SecurityTokenBitLength, securityTokenBytes); + deserializedToken.IsSessionToken = reader.ReadBoolean(); + + if (!deserializedToken.IsSessionToken) + { + bool isClaimsBased = reader.ReadBoolean(); + if (isClaimsBased) + { + byte[] claimUidBytes = reader.ReadBytes(AntiForgeryToken.ClaimUidBitLength / 8); + deserializedToken.ClaimUid = new BinaryBlob(AntiForgeryToken.ClaimUidBitLength, claimUidBytes); + } + else + { + deserializedToken.Username = reader.ReadString(); + } + + deserializedToken.AdditionalData = reader.ReadString(); + } + + // if there's still unconsumed data in the stream, fail + if (reader.BaseStream.ReadByte() != -1) + { + return null; + } + + // success + return deserializedToken; + } + + [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "MemoryStream is safe for multi-dispose.")] + public string Serialize(AntiForgeryToken token) + { + Contract.Assert(token != null); + + using (MemoryStream stream = new MemoryStream()) + { + using (BinaryWriter writer = new BinaryWriter(stream)) + { + writer.Write(TokenVersion); + writer.Write(token.SecurityToken.GetData()); + writer.Write(token.IsSessionToken); + + if (!token.IsSessionToken) + { + if (token.ClaimUid != null) + { + writer.Write(true /* isClaimsBased */); + writer.Write(token.ClaimUid.GetData()); + } + else + { + writer.Write(false /* isClaimsBased */); + writer.Write(token.Username); + } + + writer.Write(token.AdditionalData); + } + + writer.Flush(); + return _cryptoSystem.Protect(stream.ToArray()); + } + } + } + } +} diff --git a/src/System.Web.WebPages/Helpers/AntiXsrf/AntiForgeryTokenStore.cs b/src/System.Web.WebPages/Helpers/AntiXsrf/AntiForgeryTokenStore.cs new file mode 100644 index 00000000..6d7f290d --- /dev/null +++ b/src/System.Web.WebPages/Helpers/AntiXsrf/AntiForgeryTokenStore.cs @@ -0,0 +1,68 @@ +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Web.Mvc; + +namespace System.Web.Helpers.AntiXsrf +{ + // Saves anti-XSRF tokens split between HttpRequest.Cookies and HttpRequest.Form + internal sealed class AntiForgeryTokenStore : ITokenStore + { + private readonly IAntiForgeryConfig _config; + private readonly IAntiForgeryTokenSerializer _serializer; + + public AntiForgeryTokenStore() + : this(new AntiForgeryConfigWrapper(), new AntiForgeryTokenSerializer()) + { + } + + // for unit testing + internal AntiForgeryTokenStore(IAntiForgeryConfig config, IAntiForgeryTokenSerializer serializer) + { + _config = config; + _serializer = serializer; + } + + public AntiForgeryToken GetCookieToken(HttpContextBase httpContext) + { + HttpCookie cookie = httpContext.Request.Cookies[_config.CookieName]; + if (cookie == null || String.IsNullOrEmpty(cookie.Value)) + { + // did not exist + return null; + } + + return _serializer.Deserialize(cookie.Value); + } + + public AntiForgeryToken GetFormToken(HttpContextBase httpContext) + { + string value = httpContext.Request.Form[_config.FormFieldName]; + if (String.IsNullOrEmpty(value)) + { + // did not exist + return null; + } + + return _serializer.Deserialize(value); + } + + public void SaveCookieToken(HttpContextBase httpContext, AntiForgeryToken token) + { + string serializedToken = _serializer.Serialize(token); + HttpCookie newCookie = new HttpCookie(_config.CookieName, serializedToken) + { + HttpOnly = true + }; + + // Note: don't use "newCookie.Secure = _config.RequireSSL;" since the default + // value of newCookie.Secure is automatically populated from the <httpCookies> + // config element. + if (_config.RequireSSL) + { + newCookie.Secure = true; + } + + httpContext.Response.Cookies.Set(newCookie); + } + } +} diff --git a/src/System.Web.WebPages/Helpers/AntiXsrf/AntiForgeryWorker.cs b/src/System.Web.WebPages/Helpers/AntiXsrf/AntiForgeryWorker.cs new file mode 100644 index 00000000..f5a06c3c --- /dev/null +++ b/src/System.Web.WebPages/Helpers/AntiXsrf/AntiForgeryWorker.cs @@ -0,0 +1,179 @@ +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Security.Principal; +using System.Web.Mvc; +using System.Web.WebPages.Resources; + +namespace System.Web.Helpers.AntiXsrf +{ + internal sealed class AntiForgeryWorker + { + private readonly IAntiForgeryConfig _config; + private readonly IAntiForgeryTokenSerializer _serializer; + private readonly ITokenStore _tokenStore; + private readonly ITokenValidator _validator; + + public AntiForgeryWorker() + : this(new AntiForgeryTokenSerializer(), new AntiForgeryConfigWrapper(), new AntiForgeryTokenStore(), new TokenValidator()) + { + } + + // for unit testing + internal AntiForgeryWorker(IAntiForgeryTokenSerializer serializer, IAntiForgeryConfig config, ITokenStore tokenStore, ITokenValidator validator) + { + _serializer = serializer; + _config = config; + _tokenStore = tokenStore; + _validator = validator; + } + + private void CheckSSLConfig(HttpContextBase httpContext) + { + if (_config.RequireSSL && !httpContext.Request.IsSecureConnection) + { + throw new InvalidOperationException(WebPageResources.AntiForgeryWorker_RequireSSL); + } + } + + private AntiForgeryToken DeserializeToken(string serializedToken) + { + return (!String.IsNullOrEmpty(serializedToken)) + ? _serializer.Deserialize(serializedToken) + : null; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Caller will just regenerate token in case of failure.")] + private AntiForgeryToken DeserializeTokenNoThrow(string serializedToken) + { + try + { + return DeserializeToken(serializedToken); + } + catch + { + // ignore failures since we'll just generate a new token + return null; + } + } + + private static IIdentity ExtractIdentity(HttpContextBase httpContext) + { + if (httpContext != null) + { + IPrincipal user = httpContext.User; + if (user != null) + { + return user.Identity; + } + } + return null; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Caller will just regenerate token in case of failure.")] + private AntiForgeryToken GetCookieTokenNoThrow(HttpContextBase httpContext) + { + try + { + return _tokenStore.GetCookieToken(httpContext); + } + catch + { + // ignore failures since we'll just generate a new token + return null; + } + } + + // [ ENTRY POINT ] + // Generates an anti-XSRF token pair for the current user. The return + // value is the hidden input form element that should be rendered in + // the <form>. This method has a side effect: it may set a response + // cookie. + public TagBuilder GetFormInputElement(HttpContextBase httpContext) + { + CheckSSLConfig(httpContext); + + AntiForgeryToken oldCookieToken = GetCookieTokenNoThrow(httpContext); + AntiForgeryToken newCookieToken, formToken; + GetTokens(httpContext, oldCookieToken, out newCookieToken, out formToken); + + if (newCookieToken != null) + { + // If a new cookie was generated, persist it. + _tokenStore.SaveCookieToken(httpContext, newCookieToken); + } + + // <input type="hidden" name="__AntiForgeryToken" value="..." /> + TagBuilder retVal = new TagBuilder("input"); + retVal.Attributes["type"] = "hidden"; + retVal.Attributes["name"] = _config.FormFieldName; + retVal.Attributes["value"] = _serializer.Serialize(formToken); + return retVal; + } + + // [ ENTRY POINT ] + // Generates a (cookie, form) serialized token pair for the current user. + // The caller may specify an existing cookie value if one exists. If the + // 'new cookie value' out param is non-null, the caller *must* persist + // the new value to cookie storage since the original value was null or + // invalid. This method is side-effect free. + public void GetTokens(HttpContextBase httpContext, string serializedOldCookieToken, out string serializedNewCookieToken, out string serializedFormToken) + { + CheckSSLConfig(httpContext); + + AntiForgeryToken oldCookieToken = DeserializeTokenNoThrow(serializedOldCookieToken); + AntiForgeryToken newCookieToken, formToken; + GetTokens(httpContext, oldCookieToken, out newCookieToken, out formToken); + + serializedNewCookieToken = Serialize(newCookieToken); + serializedFormToken = Serialize(formToken); + } + + private void GetTokens(HttpContextBase httpContext, AntiForgeryToken oldCookieToken, out AntiForgeryToken newCookieToken, out AntiForgeryToken formToken) + { + newCookieToken = null; + if (!_validator.IsCookieTokenValid(oldCookieToken)) + { + // Need to make sure we're always operating with a good cookie token. + oldCookieToken = newCookieToken = _validator.GenerateCookieToken(); + } + + Contract.Assert(_validator.IsCookieTokenValid(oldCookieToken)); + formToken = _validator.GenerateFormToken(httpContext, ExtractIdentity(httpContext), oldCookieToken); + } + + private string Serialize(AntiForgeryToken token) + { + return (token != null) ? _serializer.Serialize(token) : null; + } + + // [ ENTRY POINT ] + // Given an HttpContext, validates that the anti-XSRF tokens contained + // in the cookies & form are OK for this request. + public void Validate(HttpContextBase httpContext) + { + CheckSSLConfig(httpContext); + + // Extract cookie & form tokens + AntiForgeryToken cookieToken = _tokenStore.GetCookieToken(httpContext); + AntiForgeryToken formToken = _tokenStore.GetFormToken(httpContext); + + // Validate + _validator.ValidateTokens(httpContext, ExtractIdentity(httpContext), cookieToken, formToken); + } + + // [ ENTRY POINT ] + // Given the serialized string representations of a cookie & form token, + // validates that the pair is OK for this request. + public void Validate(HttpContextBase httpContext, string cookieToken, string formToken) + { + CheckSSLConfig(httpContext); + + // Extract cookie & form tokens + AntiForgeryToken deserializedCookieToken = DeserializeToken(cookieToken); + AntiForgeryToken deserializedFormToken = DeserializeToken(formToken); + + // Validate + _validator.ValidateTokens(httpContext, ExtractIdentity(httpContext), deserializedCookieToken, deserializedFormToken); + } + } +} diff --git a/src/System.Web.WebPages/Helpers/AntiXsrf/BinaryBlob.cs b/src/System.Web.WebPages/Helpers/AntiXsrf/BinaryBlob.cs new file mode 100644 index 00000000..69c08cf9 --- /dev/null +++ b/src/System.Web.WebPages/Helpers/AntiXsrf/BinaryBlob.cs @@ -0,0 +1,98 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Security.Cryptography; +using System.Text; + +namespace System.Web.Helpers.AntiXsrf +{ + // Represents a binary blob (token) that contains random data. + // Useful for binary data inside a serialized stream. + [DebuggerDisplay("{DebuggerString}")] + internal sealed class BinaryBlob : IEquatable<BinaryBlob> + { + private static readonly RNGCryptoServiceProvider _prng = new RNGCryptoServiceProvider(); + + private readonly byte[] _data; + + // Generates a new token using a specified bit length. + public BinaryBlob(int bitLength) + : this(bitLength, GenerateNewToken(bitLength)) + { + } + + // Generates a token using an existing binary value. + public BinaryBlob(int bitLength, byte[] data) + { + if (bitLength < 32 || bitLength % 8 != 0) + { + throw new ArgumentOutOfRangeException("bitLength"); + } + if (data == null || data.Length != bitLength / 8) + { + throw new ArgumentOutOfRangeException("data"); + } + + _data = data; + } + + public int BitLength + { + get + { + return checked(_data.Length * 8); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by debugger.")] + private string DebuggerString + { + get + { + StringBuilder sb = new StringBuilder("0x", 2 + (_data.Length * 2)); + for (int i = 0; i < _data.Length; i++) + { + sb.AppendFormat(CultureInfo.InvariantCulture, "{0:x2}", _data[i]); + } + return sb.ToString(); + } + } + + public override bool Equals(object obj) + { + return Equals(obj as BinaryBlob); + } + + public bool Equals(BinaryBlob other) + { + if (other == null) + { + return false; + } + + Contract.Assert(this._data.Length == other._data.Length); + return CryptoUtil.AreByteArraysEqual(this._data, other._data); + } + + public byte[] GetData() + { + return _data; + } + + public override int GetHashCode() + { + // Since data should contain uniformly-distributed entropy, the + // first 32 bits can serve as the hash code. + Contract.Assert(_data != null && _data.Length >= (32 / 8)); + return BitConverter.ToInt32(_data, 0); + } + + private static byte[] GenerateNewToken(int bitLength) + { + byte[] data = new byte[bitLength / 8]; + _prng.GetBytes(data); + return data; + } + } +} diff --git a/src/System.Web.WebPages/Helpers/AntiXsrf/ClaimUidExtractor.cs b/src/System.Web.WebPages/Helpers/AntiXsrf/ClaimUidExtractor.cs new file mode 100644 index 00000000..3c4b207f --- /dev/null +++ b/src/System.Web.WebPages/Helpers/AntiXsrf/ClaimUidExtractor.cs @@ -0,0 +1,97 @@ +using System.Globalization; +using System.Linq; +using System.Security.Principal; +using System.Web.Helpers.Claims; +using System.Web.WebPages.Resources; + +namespace System.Web.Helpers.AntiXsrf +{ + internal sealed class ClaimUidExtractor : IClaimUidExtractor + { + internal const string NameIdentifierClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"; + internal const string IdentityProviderClaimType = "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider"; + + private readonly ClaimsIdentityConverter _claimsIdentityConverter; + private readonly IAntiForgeryConfig _config; + + public ClaimUidExtractor() + : this(new AntiForgeryConfigWrapper(), ClaimsIdentityConverter.Default) + { + } + + // for unit testing + internal ClaimUidExtractor(IAntiForgeryConfig config, ClaimsIdentityConverter claimsIdentityConverter) + { + _config = config; + _claimsIdentityConverter = claimsIdentityConverter; + } + + public BinaryBlob ExtractClaimUid(IIdentity identity) + { + if (identity == null || !identity.IsAuthenticated || _config.SuppressIdentityHeuristicChecks) + { + // Skip anonymous users + // Skip when claims-based checks are disabled + return null; + } + + ClaimsIdentity claimsIdentity = _claimsIdentityConverter.TryConvert(identity); + if (claimsIdentity == null) + { + // not a claims-based identity + return null; + } + + string[] uniqueIdentifierParameters = GetUniqueIdentifierParameters(claimsIdentity, _config.UniqueClaimTypeIdentifier); + byte[] claimUidBytes = CryptoUtil.ComputeSHA256(uniqueIdentifierParameters); + return new BinaryBlob(256, claimUidBytes); + } + + internal static string[] GetUniqueIdentifierParameters(ClaimsIdentity claimsIdentity, string uniqueClaimTypeIdentifier) + { + var claims = claimsIdentity.GetClaims(); + + // The application developer might not want to use our default behavior + // and instead might want us to use a claim he knows is unique within + // the security realm of his application. (Perhaps he has crafted this + // claim himself.) + if (!String.IsNullOrEmpty(uniqueClaimTypeIdentifier)) + { + Claim matchingClaim = claims.SingleOrDefault(claim => String.Equals(uniqueClaimTypeIdentifier, claim.ClaimType, StringComparison.Ordinal)); + if (matchingClaim == null || String.IsNullOrEmpty(matchingClaim.Value)) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebPageResources.ClaimUidExtractor_ClaimNotPresent, uniqueClaimTypeIdentifier)); + } + + return new string[] + { + uniqueClaimTypeIdentifier, + matchingClaim.Value + }; + } + + // By default, we look for 'nameIdentifier' and 'identityProvider' claims. + // For a correctly configured ACS consumer, this tuple will uniquely + // identify a user of the application. We assume that a well-behaved + // identity provider will never assign the same name identifier to multiple + // users within its security realm, and we assume that ACS has been + // configured so that each identity provider has a unique 'identityProvider' + // claim. + Claim nameIdentifierClaim = claims.SingleOrDefault(claim => String.Equals(NameIdentifierClaimType, claim.ClaimType, StringComparison.Ordinal)); + Claim identityProviderClaim = claims.SingleOrDefault(claim => String.Equals(IdentityProviderClaimType, claim.ClaimType, StringComparison.Ordinal)); + if (nameIdentifierClaim == null || String.IsNullOrEmpty(nameIdentifierClaim.Value) + || identityProviderClaim == null || String.IsNullOrEmpty(identityProviderClaim.Value)) + { + throw new InvalidOperationException(WebPageResources.ClaimUidExtractor_DefaultClaimsNotPresent); + } + + return new string[] + { + NameIdentifierClaimType, + nameIdentifierClaim.Value, + IdentityProviderClaimType, + identityProviderClaim.Value + }; + } + } +} diff --git a/src/System.Web.WebPages/Helpers/AntiXsrf/IAntiForgeryConfig.cs b/src/System.Web.WebPages/Helpers/AntiXsrf/IAntiForgeryConfig.cs new file mode 100644 index 00000000..76a31b0f --- /dev/null +++ b/src/System.Web.WebPages/Helpers/AntiXsrf/IAntiForgeryConfig.cs @@ -0,0 +1,24 @@ +namespace System.Web.Helpers.AntiXsrf +{ + // Provides configuration information about the anti-forgery system. + internal interface IAntiForgeryConfig + { + // Provides additional data to go into the tokens. + IAntiForgeryAdditionalDataProvider AdditionalDataProvider { get; } + + // Name of the cookie to use. + string CookieName { get; } + + // Name of the form field to use. + string FormFieldName { get; } + + // Whether SSL is mandatory for this request. + bool RequireSSL { get; } + + // Skip ClaimsIdentity & related logic. + bool SuppressIdentityHeuristicChecks { get; } + + // ClaimType to use for ClaimsIdentity. + string UniqueClaimTypeIdentifier { get; } + } +} diff --git a/src/System.Web.WebPages/Helpers/AntiXsrf/IAntiForgeryTokenSerializer.cs b/src/System.Web.WebPages/Helpers/AntiXsrf/IAntiForgeryTokenSerializer.cs new file mode 100644 index 00000000..a80c52e3 --- /dev/null +++ b/src/System.Web.WebPages/Helpers/AntiXsrf/IAntiForgeryTokenSerializer.cs @@ -0,0 +1,9 @@ +namespace System.Web.Helpers.AntiXsrf +{ + // Abstracts out the serialization process for an anti-forgery token + internal interface IAntiForgeryTokenSerializer + { + AntiForgeryToken Deserialize(string serializedToken); + string Serialize(AntiForgeryToken token); + } +} diff --git a/src/System.Web.WebPages/Helpers/AntiXsrf/IClaimUidExtractor.cs b/src/System.Web.WebPages/Helpers/AntiXsrf/IClaimUidExtractor.cs new file mode 100644 index 00000000..bf54ca13 --- /dev/null +++ b/src/System.Web.WebPages/Helpers/AntiXsrf/IClaimUidExtractor.cs @@ -0,0 +1,10 @@ +using System.Security.Principal; + +namespace System.Web.Helpers.AntiXsrf +{ + // Can extract unique identifers for a claims-based identity + internal interface IClaimUidExtractor + { + BinaryBlob ExtractClaimUid(IIdentity identity); + } +} diff --git a/src/System.Web.WebPages/Helpers/AntiXsrf/ICryptoSystem.cs b/src/System.Web.WebPages/Helpers/AntiXsrf/ICryptoSystem.cs new file mode 100644 index 00000000..48ee03f2 --- /dev/null +++ b/src/System.Web.WebPages/Helpers/AntiXsrf/ICryptoSystem.cs @@ -0,0 +1,9 @@ +namespace System.Web.Helpers.AntiXsrf +{ + // Provides an abstraction around the cryptographic subsystem for the anti-XSRF helpers. + internal interface ICryptoSystem + { + string Protect(byte[] data); + byte[] Unprotect(string protectedData); + } +} diff --git a/src/System.Web.WebPages/Helpers/AntiXsrf/ITokenStore.cs b/src/System.Web.WebPages/Helpers/AntiXsrf/ITokenStore.cs new file mode 100644 index 00000000..19ac008f --- /dev/null +++ b/src/System.Web.WebPages/Helpers/AntiXsrf/ITokenStore.cs @@ -0,0 +1,10 @@ +namespace System.Web.Helpers.AntiXsrf +{ + // Provides an abstraction around how tokens are persisted and retrieved for a request + internal interface ITokenStore + { + AntiForgeryToken GetCookieToken(HttpContextBase httpContext); + AntiForgeryToken GetFormToken(HttpContextBase httpContext); + void SaveCookieToken(HttpContextBase httpContext, AntiForgeryToken token); + } +} diff --git a/src/System.Web.WebPages/Helpers/AntiXsrf/ITokenValidator.cs b/src/System.Web.WebPages/Helpers/AntiXsrf/ITokenValidator.cs new file mode 100644 index 00000000..42110155 --- /dev/null +++ b/src/System.Web.WebPages/Helpers/AntiXsrf/ITokenValidator.cs @@ -0,0 +1,22 @@ +using System.Security.Principal; + +namespace System.Web.Helpers.AntiXsrf +{ + // Provides an abstraction around something that can validate anti-XSRF tokens + internal interface ITokenValidator + { + // Generates a new random cookie token. + AntiForgeryToken GenerateCookieToken(); + + // Given a cookie token, generates a corresponding form token. + // The incoming cookie token must be valid. + AntiForgeryToken GenerateFormToken(HttpContextBase httpContext, IIdentity identity, AntiForgeryToken cookieToken); + + // Determines whether an existing cookie token is valid (well-formed). + // If it is not, the caller must call GenerateCookieToken() before calling GenerateFormToken(). + bool IsCookieTokenValid(AntiForgeryToken cookieToken); + + // Validates a (cookie, form) token pair. + void ValidateTokens(HttpContextBase httpContext, IIdentity identity, AntiForgeryToken cookieToken, AntiForgeryToken formToken); + } +} diff --git a/src/System.Web.WebPages/Helpers/AntiXsrf/MachineKeyCryptoSystem.cs b/src/System.Web.WebPages/Helpers/AntiXsrf/MachineKeyCryptoSystem.cs new file mode 100644 index 00000000..ad1e4bc5 --- /dev/null +++ b/src/System.Web.WebPages/Helpers/AntiXsrf/MachineKeyCryptoSystem.cs @@ -0,0 +1,97 @@ +using System.Net; +using System.Text; +using System.Web.Security; + +namespace System.Web.Helpers.AntiXsrf +{ + // Interfaces with the System.Web.MachineKey static class + internal sealed class MachineKeyCryptoSystem : ICryptoSystem + { + // This is the magic header that identifies an AntiForgeryToken payload. + // It helps differentiate this from other encrypted payloads. + private const uint MagicHeader = 0x8587f266; + + private readonly Func<string, MachineKeyProtection, byte[]> _decoder; + private readonly Func<byte[], MachineKeyProtection, string> _encoder; + + public MachineKeyCryptoSystem() + : this(MachineKey.Encode, MachineKey.Decode) + { + } + + // for unit testing + internal MachineKeyCryptoSystem(Func<byte[], MachineKeyProtection, string> encoder, Func<string, MachineKeyProtection, byte[]> decoder) + { + _encoder = encoder; + _decoder = decoder; + } + + public string Protect(byte[] data) + { + byte[] dataWithHeader = new byte[data.Length + 4]; + Buffer.BlockCopy(data, 0, dataWithHeader, 4, data.Length); + unchecked + { + dataWithHeader[0] = (byte)(MagicHeader >> 24); + dataWithHeader[1] = (byte)(MagicHeader >> 16); + dataWithHeader[2] = (byte)(MagicHeader >> 8); + dataWithHeader[3] = (byte)(MagicHeader); + } + + string hex = _encoder(dataWithHeader, MachineKeyProtection.All); + return HexToBase64(hex); + } + + public byte[] Unprotect(string protectedData) + { + string hex = Base64ToHex(protectedData); + byte[] dataWithHeader = _decoder(hex, MachineKeyProtection.All); + + if (dataWithHeader == null || dataWithHeader.Length < 4 || (uint)IPAddress.NetworkToHostOrder(BitConverter.ToInt32(dataWithHeader, 0)) != MagicHeader) + { + // the decoded data is blank or doesn't begin with the magic header + return null; + } + + byte[] retVal = new byte[dataWithHeader.Length - 4]; + Buffer.BlockCopy(dataWithHeader, 4, retVal, 0, retVal.Length); + return retVal; + } + + // String transformation helpers + + internal static string Base64ToHex(string base64) + { + StringBuilder builder = new StringBuilder((int)(base64.Length * 1.5)); + foreach (byte b in HttpServerUtility.UrlTokenDecode(base64)) + { + builder.Append(HexDigit(b >> 4)); + builder.Append(HexDigit(b & 0x0F)); + } + string result = builder.ToString(); + return result; + } + + private static char HexDigit(int value) + { + return (char)(value > 9 ? value + '7' : value + '0'); + } + + private static int HexValue(char digit) + { + return digit > '9' ? digit - '7' : digit - '0'; + } + + internal static string HexToBase64(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])); + } + string result = HttpServerUtility.UrlTokenEncode(bytes); + return result; + } + } +} diff --git a/src/System.Web.WebPages/Helpers/AntiXsrf/TokenValidator.cs b/src/System.Web.WebPages/Helpers/AntiXsrf/TokenValidator.cs new file mode 100644 index 00000000..67625598 --- /dev/null +++ b/src/System.Web.WebPages/Helpers/AntiXsrf/TokenValidator.cs @@ -0,0 +1,145 @@ +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Security.Principal; +using System.Web.Mvc; +using System.Web.WebPages.Resources; + +namespace System.Web.Helpers.AntiXsrf +{ + internal sealed class TokenValidator : ITokenValidator + { + private readonly IClaimUidExtractor _claimUidExtractor; + private readonly IAntiForgeryConfig _config; + + public TokenValidator() + : this(new AntiForgeryConfigWrapper(), new ClaimUidExtractor()) + { + } + + // for unit testing + internal TokenValidator(IAntiForgeryConfig config, IClaimUidExtractor claimUidExtractor) + { + _config = config; + _claimUidExtractor = claimUidExtractor; + } + + public AntiForgeryToken GenerateCookieToken() + { + return new AntiForgeryToken() + { + // SecurityToken will be populated automatically. + IsSessionToken = true + }; + } + + public AntiForgeryToken GenerateFormToken(HttpContextBase httpContext, IIdentity identity, AntiForgeryToken cookieToken) + { + Contract.Assert(IsCookieTokenValid(cookieToken)); + + AntiForgeryToken formToken = new AntiForgeryToken() + { + SecurityToken = cookieToken.SecurityToken, + IsSessionToken = false + }; + + bool requireAuthenticatedUserHeuristicChecks = false; + // populate Username and ClaimUid + if (identity != null && identity.IsAuthenticated) + { + if (!_config.SuppressIdentityHeuristicChecks) + { + // If the user is authenticated and heuristic checks are not suppressed, + // then Username, ClaimUid, or AdditionalData must be set. + requireAuthenticatedUserHeuristicChecks = true; + } + + formToken.ClaimUid = _claimUidExtractor.ExtractClaimUid(identity); + if (formToken.ClaimUid == null) + { + formToken.Username = identity.Name; + } + } + + // populate AdditionalData + if (_config.AdditionalDataProvider != null) + { + formToken.AdditionalData = _config.AdditionalDataProvider.GetAdditionalData(httpContext); + } + + if (requireAuthenticatedUserHeuristicChecks + && String.IsNullOrEmpty(formToken.Username) + && formToken.ClaimUid == null + && String.IsNullOrEmpty(formToken.AdditionalData)) + { + // Application says user is authenticated, but we have no identifier for the user. + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, + WebPageResources.TokenValidator_AuthenticatedUserWithoutUsername, identity.GetType())); + } + + return formToken; + } + + public bool IsCookieTokenValid(AntiForgeryToken cookieToken) + { + return (cookieToken != null && cookieToken.IsSessionToken); + } + + public void ValidateTokens(HttpContextBase httpContext, IIdentity identity, AntiForgeryToken sessionToken, AntiForgeryToken fieldToken) + { + // Were the tokens even present at all? + if (sessionToken == null) + { + throw HttpAntiForgeryException.CreateCookieMissingException(_config.CookieName); + } + if (fieldToken == null) + { + throw HttpAntiForgeryException.CreateFormFieldMissingException(_config.FormFieldName); + } + + // Do the tokens have the correct format? + if (!sessionToken.IsSessionToken || fieldToken.IsSessionToken) + { + throw HttpAntiForgeryException.CreateTokensSwappedException(_config.CookieName, _config.FormFieldName); + } + + // Are the security tokens embedded in each incoming token identical? + if (!Equals(sessionToken.SecurityToken, fieldToken.SecurityToken)) + { + throw HttpAntiForgeryException.CreateSecurityTokenMismatchException(); + } + + // Is the incoming token meant for the current user? + string currentUsername = String.Empty; + BinaryBlob currentClaimUid = null; + + if (identity != null && identity.IsAuthenticated) + { + currentClaimUid = _claimUidExtractor.ExtractClaimUid(identity); + if (currentClaimUid == null) + { + currentUsername = identity.Name ?? String.Empty; + } + } + + // OpenID and other similar authentication schemes use URIs for the username. + // These should be treated as case-sensitive. + bool useCaseSensitiveUsernameComparison = currentUsername.StartsWith("http://", StringComparison.OrdinalIgnoreCase) + || currentUsername.StartsWith("https://", StringComparison.OrdinalIgnoreCase); + + if (!String.Equals(fieldToken.Username, currentUsername, (useCaseSensitiveUsernameComparison) ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)) + { + throw HttpAntiForgeryException.CreateUsernameMismatchException(fieldToken.Username, currentUsername); + } + if (!Equals(fieldToken.ClaimUid, currentClaimUid)) + { + throw HttpAntiForgeryException.CreateClaimUidMismatchException(); + } + + // Is the AdditionalData valid? + if (_config.AdditionalDataProvider != null && !_config.AdditionalDataProvider.ValidateAdditionalData(httpContext, fieldToken.AdditionalData)) + { + throw HttpAntiForgeryException.CreateAdditionalDataCheckFailedException(); + } + } + } +} diff --git a/src/System.Web.WebPages/Helpers/Claims/Claim.cs b/src/System.Web.WebPages/Helpers/Claims/Claim.cs new file mode 100644 index 00000000..f68b345e --- /dev/null +++ b/src/System.Web.WebPages/Helpers/Claims/Claim.cs @@ -0,0 +1,69 @@ +using System.Reflection; + +namespace System.Web.Helpers.Claims +{ + // Represents a Claim; serves as an abstraction around the WIF SDK and 4.5 Claim types since + // we can't compile directly against them. + internal sealed class Claim + { + public Claim(string claimType, string value) + { + ClaimType = claimType; + Value = value; + } + + public string ClaimType { get; private set; } + + public string Value { get; private set; } + + // Creates a Claim from a TClaim object (duck typing). + // + // The TClaim must have the following shape: + // class TClaim { + // string ClaimType { get; } // or just 'Type' + // string Value { get; } + // } + internal static Claim Create<TClaim>(TClaim claim) + { + return ClaimFactory<TClaim>.Create(claim); + } + + private static class ClaimFactory<TClaim> + { + private static readonly Func<TClaim, string> _claimTypeGetter = CreateClaimTypeGetter(); + private static readonly Func<TClaim, string> _valueGetter = CreateValueGetter(); + + public static Claim Create(TClaim claim) + { + return new Claim(_claimTypeGetter(claim), _valueGetter(claim)); + } + + private static Func<TClaim, string> CreateClaimTypeGetter() + { + // the claim type might go by one of two different property names + return CreateGeneralPropertyGetter("ClaimType") ?? CreateGeneralPropertyGetter("Type"); + } + + private static Func<TClaim, string> CreateGeneralPropertyGetter(string propertyName) + { + PropertyInfo propInfo = typeof(TClaim).GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance, null, typeof(string), Type.EmptyTypes, null); + if (propInfo == null) + { + return null; + } + + MethodInfo propGetter = propInfo.GetGetMethod(); + + // For improved perf, instance methods can be treated as static methods by leaving + // the 'this' parameter unbound. Virtual dispatch for the property getter will + // still take place as expected. + return (Func<TClaim, string>)Delegate.CreateDelegate(typeof(Func<TClaim, string>), propGetter); + } + + private static Func<TClaim, string> CreateValueGetter() + { + return CreateGeneralPropertyGetter("Value"); + } + } + } +} diff --git a/src/System.Web.WebPages/Helpers/Claims/ClaimsIdentity.cs b/src/System.Web.WebPages/Helpers/Claims/ClaimsIdentity.cs new file mode 100644 index 00000000..f2171b91 --- /dev/null +++ b/src/System.Web.WebPages/Helpers/Claims/ClaimsIdentity.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Security.Principal; + +namespace System.Web.Helpers.Claims +{ + // Represents a ClaimsIdentity; serves as an abstraction around the WIF SDK and 4.5 + // ClaimIdentity types since we can't compile directly against them. + internal abstract class ClaimsIdentity + { + public abstract IEnumerable<Claim> GetClaims(); + + // Attempts to convert an IIdentity into a ClaimsIdentity; + // returns null if the conversion fails (duck typing). + // + // The TClaimsIdentity must have the following shape: + // class TClaimsIdentity : IIdentity { + // TClaimsCollection Claims { get; } + // } + // where TClaimsCollection is assignable to IEnumerable<TClaim>, + // and where TClaim is valid for Claim.Create<TClaim>. + internal static ClaimsIdentity TryConvert<TClaimsIdentity, TClaim>(IIdentity identity) + where TClaimsIdentity : class, IIdentity + { + TClaimsIdentity castClaimsIdentity = identity as TClaimsIdentity; + return (castClaimsIdentity != null) + ? new ClaimsIdentityImpl<TClaimsIdentity, TClaim>(castClaimsIdentity) + : null; + } + + private sealed class ClaimsIdentityImpl<TClaimsIdentity, TClaim> : ClaimsIdentity + where TClaimsIdentity : class, IIdentity + { + private static readonly Func<TClaimsIdentity, IEnumerable<TClaim>> _claimsGetter = CreateClaimsGetter(); + + private readonly TClaimsIdentity _claimsIdentity; + + public ClaimsIdentityImpl(TClaimsIdentity claimsIdentity) + { + _claimsIdentity = claimsIdentity; + } + + private static Func<TClaimsIdentity, IEnumerable<TClaim>> CreateClaimsGetter() + { + PropertyInfo propInfo = typeof(TClaimsIdentity).GetProperty("Claims", BindingFlags.Public | BindingFlags.Instance); + MethodInfo propGetter = propInfo.GetGetMethod(); + + // For improved perf, instance methods can be treated as static methods by leaving + // the 'this' parameter unbound. Virtual dispatch for the property getter will + // still take place as expected. + return (Func<TClaimsIdentity, IEnumerable<TClaim>>)Delegate.CreateDelegate(typeof(Func<TClaimsIdentity, IEnumerable<TClaim>>), propGetter); + } + + public override IEnumerable<Claim> GetClaims() + { + return _claimsGetter(_claimsIdentity).Select(Claim.Create); + } + } + } +} diff --git a/src/System.Web.WebPages/Helpers/Claims/ClaimsIdentityConverter.cs b/src/System.Web.WebPages/Helpers/Claims/ClaimsIdentityConverter.cs new file mode 100644 index 00000000..b35c4a14 --- /dev/null +++ b/src/System.Web.WebPages/Helpers/Claims/ClaimsIdentityConverter.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Security.Principal; +using System.Web.Security; + +namespace System.Web.Helpers.Claims +{ + // Can convert IIdentity instances into our ClaimsIdentity wrapper. + internal sealed class ClaimsIdentityConverter + { + private static readonly MethodInfo _claimsIdentityTryConvertOpenMethod = typeof(ClaimsIdentity).GetMethod("TryConvert", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); + private static readonly ClaimsIdentityConverter _default = new ClaimsIdentityConverter(GetDefaultConverters()); + + private readonly Func<IIdentity, ClaimsIdentity>[] _converters; + + // Internal for unit testing; nobody should ever be calling this in production. + internal ClaimsIdentityConverter(Func<IIdentity, ClaimsIdentity>[] converters) + { + _converters = converters; + } + + // By default, we understand the ClaimsIdentity / Claim types included + // with the WIF SDK and FX 4.5. + public static ClaimsIdentityConverter Default + { + get + { + return _default; + } + } + + private static bool IsGrandfatheredIdentityType(IIdentity claimsIdentity) + { + // These specific types might also be claims-based types depending on + // the version of the framework we're running, but we don't want to + // treat them as claims-based types since we know their Name property + // will suffice as a unique identifier within the security realm of the + // current application. + return claimsIdentity is FormsIdentity + || claimsIdentity is WindowsIdentity + || claimsIdentity is GenericIdentity; + } + + public ClaimsIdentity TryConvert(IIdentity identity) + { + if (IsGrandfatheredIdentityType(identity)) + { + return null; + } + + // loop through all registered converters until one matches + for (int i = 0; i < _converters.Length; i++) + { + ClaimsIdentity retVal = _converters[i](identity); + if (retVal != null) + { + return retVal; + } + } + + return null; + } + + private static void AddToList(IList<Func<IIdentity, ClaimsIdentity>> converters, Type claimsIdentityType, Type claimType) + { + if (claimsIdentityType != null && claimType != null) + { + MethodInfo tryConvertClosedMethod = _claimsIdentityTryConvertOpenMethod.MakeGenericMethod(claimsIdentityType, claimType); + Func<IIdentity, ClaimsIdentity> converter = (Func<IIdentity, ClaimsIdentity>)Delegate.CreateDelegate(typeof(Func<IIdentity, ClaimsIdentity>), tryConvertClosedMethod); + converters.Add(converter); + } + } + + private static Func<IIdentity, ClaimsIdentity>[] GetDefaultConverters() + { + List<Func<IIdentity, ClaimsIdentity>> converters = new List<Func<IIdentity, ClaimsIdentity>>(); + + // WIF SDK is only available in full trust scenarios + if (AppDomain.CurrentDomain.IsHomogenous && AppDomain.CurrentDomain.IsFullyTrusted) + { + Type claimsIdentityType = Type.GetType("Microsoft.IdentityModel.Claims.IClaimsIdentity, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"); + Type claimType = Type.GetType("Microsoft.IdentityModel.Claims.Claim, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"); + AddToList(converters, claimsIdentityType, claimType); + } + + // 4.5 ClaimsIdentity type + { + Module mscorlibModule = typeof(object).Module; + Type claimsIdentityType = mscorlibModule.GetType("System.Security.Claims.ClaimsIdentity"); + Type claimType = mscorlibModule.GetType("System.Security.Claims.Claim"); + AddToList(converters, claimsIdentityType, claimType); + } + + return converters.ToArray(); + } + } +} diff --git a/src/System.Web.WebPages/Helpers/CryptoUtil.cs b/src/System.Web.WebPages/Helpers/CryptoUtil.cs new file mode 100644 index 00000000..9555cced --- /dev/null +++ b/src/System.Web.WebPages/Helpers/CryptoUtil.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Security.Cryptography; + +namespace System.Web.Helpers +{ + internal static class CryptoUtil + { + private static readonly Func<SHA256> _sha256Factory = GetSHA256Factory(); + + // This method is specially written to take the same amount of time + // regardless of where 'a' and 'b' differ. Please do not optimize it. + public static bool AreByteArraysEqual(byte[] a, byte[] b) + { + if (a == null || b == null || a.Length != b.Length) + { + return false; + } + + bool areEqual = true; + for (int i = 0; i < a.Length; i++) + { + areEqual &= (a[i] == b[i]); + } + return areEqual; + } + + // Computes a SHA256 hash over all of the input parameters. + // Each parameter is UTF8 encoded and preceded by a 7-bit encoded + // integer describing the encoded byte length of the string. + [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "MemoryStream is resilient to double-Dispose")] + public static byte[] ComputeSHA256(IList<string> parameters) + { + using (MemoryStream ms = new MemoryStream()) + { + using (BinaryWriter bw = new BinaryWriter(ms)) + { + 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; + } + } + } + } + + 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(); + } + } +} diff --git a/src/System.Web.WebPages/Helpers/IAntiForgeryAdditionalDataProvider.cs b/src/System.Web.WebPages/Helpers/IAntiForgeryAdditionalDataProvider.cs new file mode 100644 index 00000000..0043fbab --- /dev/null +++ b/src/System.Web.WebPages/Helpers/IAntiForgeryAdditionalDataProvider.cs @@ -0,0 +1,34 @@ +namespace System.Web.Helpers +{ + /// <summary> + /// Allows providing or validating additional custom data for anti-forgery tokens. + /// For example, the developer could use this to supply a nonce when the token is + /// generated, then he could validate the nonce when the token is validated. + /// </summary> + /// <remarks> + /// The anti-forgery system already embeds the client's username within the + /// generated tokens. This interface provides and consumes <em>supplemental</em> + /// data. If an incoming anti-forgery token contains supplemental data but no + /// additional data provider is configured, the supplemental data will not be + /// validated. + /// </remarks> + public interface IAntiForgeryAdditionalDataProvider + { + /// <summary> + /// Provides additional data to be stored for the anti-forgery tokens generated + /// during this request. + /// </summary> + /// <param name="context">Information about the current request.</param> + /// <returns>Supplemental data to embed within the anti-forgery token.</returns> + string GetAdditionalData(HttpContextBase context); + + /// <summary> + /// Validates additional data that was embedded inside an incoming anti-forgery + /// token. + /// </summary> + /// <param name="context">Information about the current request.</param> + /// <param name="additionalData">Supplemental data that was embedded within the token.</param> + /// <returns>True if the data is valid; false if the data is invalid.</returns> + bool ValidateAdditionalData(HttpContextBase context, string additionalData); + } +} diff --git a/src/System.Web.WebPages/Mvc/HttpAntiForgeryException.cs b/src/System.Web.WebPages/Mvc/HttpAntiForgeryException.cs index 4d987bb4..580fc9d0 100644 --- a/src/System.Web.WebPages/Mvc/HttpAntiForgeryException.cs +++ b/src/System.Web.WebPages/Mvc/HttpAntiForgeryException.cs @@ -1,5 +1,7 @@ -using System.Runtime.CompilerServices; +using System.Globalization; +using System.Runtime.CompilerServices; using System.Runtime.Serialization; +using System.Web.WebPages.Resources; namespace System.Web.Mvc { @@ -21,9 +23,54 @@ namespace System.Web.Mvc { } + private HttpAntiForgeryException(string message, params object[] args) + : this(String.Format(CultureInfo.CurrentCulture, message, args)) + { + } + public HttpAntiForgeryException(string message, Exception innerException) : base(message, innerException) { } + + internal static HttpAntiForgeryException CreateAdditionalDataCheckFailedException() + { + return new HttpAntiForgeryException(WebPageResources.AntiForgeryToken_AdditionalDataCheckFailed); + } + + internal static HttpAntiForgeryException CreateClaimUidMismatchException() + { + return new HttpAntiForgeryException(WebPageResources.AntiForgeryToken_ClaimUidMismatch); + } + + internal static HttpAntiForgeryException CreateCookieMissingException(string cookieName) + { + return new HttpAntiForgeryException(WebPageResources.AntiForgeryToken_CookieMissing, cookieName); + } + + internal static HttpAntiForgeryException CreateDeserializationFailedException() + { + return new HttpAntiForgeryException(WebPageResources.AntiForgeryToken_DeserializationFailed); + } + + internal static HttpAntiForgeryException CreateFormFieldMissingException(string formFieldName) + { + return new HttpAntiForgeryException(WebPageResources.AntiForgeryToken_FormFieldMissing, formFieldName); + } + + internal static HttpAntiForgeryException CreateSecurityTokenMismatchException() + { + return new HttpAntiForgeryException(WebPageResources.AntiForgeryToken_SecurityTokenMismatch); + } + + internal static HttpAntiForgeryException CreateTokensSwappedException(string cookieName, string formFieldName) + { + return new HttpAntiForgeryException(WebPageResources.AntiForgeryToken_TokensSwapped, cookieName, formFieldName); + } + + internal static HttpAntiForgeryException CreateUsernameMismatchException(string usernameInToken, string currentUsername) + { + return new HttpAntiForgeryException(WebPageResources.AntiForgeryToken_UsernameMismatch, usernameInToken, currentUsername); + } } } diff --git a/src/System.Web.WebPages/Mvc/TagBuilder.cs b/src/System.Web.WebPages/Mvc/TagBuilder.cs index 47bd8c29..8ad0e1d5 100644 --- a/src/System.Web.WebPages/Mvc/TagBuilder.cs +++ b/src/System.Web.WebPages/Mvc/TagBuilder.cs @@ -176,7 +176,7 @@ namespace System.Web.Mvc InnerHtml = HttpUtility.HtmlEncode(innerText); } - internal IHtmlString ToHtmlString(TagRenderMode renderMode) + internal HtmlString ToHtmlString(TagRenderMode renderMode) { return new HtmlString(ToString(renderMode)); } diff --git a/src/System.Web.WebPages/Resources/WebPageResources.Designer.cs b/src/System.Web.WebPages/Resources/WebPageResources.Designer.cs index f374f093..e5ba177f 100644 --- a/src/System.Web.WebPages/Resources/WebPageResources.Designer.cs +++ b/src/System.Web.WebPages/Resources/WebPageResources.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.261 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -61,11 +61,83 @@ namespace System.Web.WebPages.Resources { } /// <summary> - /// Looks up a localized string similar to A required anti-forgery token was not supplied or was invalid.. + /// Looks up a localized string similar to The provided anti-forgery token failed a custom data check.. /// </summary> - internal static string AntiForgeryToken_ValidationFailed { + internal static string AntiForgeryToken_AdditionalDataCheckFailed { get { - return ResourceManager.GetString("AntiForgeryToken_ValidationFailed", resourceCulture); + return ResourceManager.GetString("AntiForgeryToken_AdditionalDataCheckFailed", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The provided anti-forgery token was meant for a different claims-based user than the current user.. + /// </summary> + internal static string AntiForgeryToken_ClaimUidMismatch { + get { + return ResourceManager.GetString("AntiForgeryToken_ClaimUidMismatch", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The required anti-forgery cookie "{0}" is not present.. + /// </summary> + internal static string AntiForgeryToken_CookieMissing { + get { + return ResourceManager.GetString("AntiForgeryToken_CookieMissing", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The anti-forgery token could not be decrypted. If this application is hosted by a Web Farm or cluster, ensure that all machines are running the same version of ASP.NET Web Pages and that the <machineKey> configuration specifies explicit encryption and validation keys. AutoGenerate cannot be used in a cluster.. + /// </summary> + internal static string AntiForgeryToken_DeserializationFailed { + get { + return ResourceManager.GetString("AntiForgeryToken_DeserializationFailed", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The required anti-forgery form field "{0}" is not present.. + /// </summary> + internal static string AntiForgeryToken_FormFieldMissing { + get { + return ResourceManager.GetString("AntiForgeryToken_FormFieldMissing", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The anti-forgery cookie token and form field token do not match.. + /// </summary> + internal static string AntiForgeryToken_SecurityTokenMismatch { + get { + return ResourceManager.GetString("AntiForgeryToken_SecurityTokenMismatch", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Validation of the provided anti-forgery token failed. The cookie "{0}" and the form field "{1}" were swapped.. + /// </summary> + internal static string AntiForgeryToken_TokensSwapped { + get { + return ResourceManager.GetString("AntiForgeryToken_TokensSwapped", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The provided anti-forgery token was meant for user "{0}", but the current user is "{1}".. + /// </summary> + internal static string AntiForgeryToken_UsernameMismatch { + get { + return ResourceManager.GetString("AntiForgeryToken_UsernameMismatch", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The anti-forgery system has the configuration value AntiForgeryConfig.RequireSsl = true, but the current request is not an SSL request.. + /// </summary> + internal static string AntiForgeryWorker_RequireSSL { + get { + return ResourceManager.GetString("AntiForgeryWorker_RequireSSL", resourceCulture); } } @@ -115,6 +187,24 @@ namespace System.Web.WebPages.Resources { } /// <summary> + /// Looks up a localized string similar to A claim of type '{0}' was not present on the provided ClaimsIdentity.. + /// </summary> + internal static string ClaimUidExtractor_ClaimNotPresent { + get { + return ResourceManager.GetString("ClaimUidExtractor_ClaimNotPresent", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to A claim of type 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier' or 'http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider' was not present on the provided ClaimsIdentity. To enable anti-forgery token support with claims-based authentication, please verify that the configured claims provider is providing both of these claims on the ClaimsIdentity instances it generates. If the configured claims provider instead uses a different claim type as a unique identif [rest of string was truncated]";. + /// </summary> + internal static string ClaimUidExtractor_DefaultClaimsNotPresent { + get { + return ResourceManager.GetString("ClaimUidExtractor_DefaultClaimsNotPresent", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Index length must be exactly one.. /// </summary> internal static string DynamicDictionary_InvalidNumberOfIndexes { @@ -214,6 +304,15 @@ namespace System.Web.WebPages.Resources { } /// <summary> + /// Looks up a localized string similar to The provided identity of type '{0}' is marked IsAuthenticated = true but does not have a value for Name. By default, the anti-forgery system requires that all authenticated identities have a unique Name. If it is not possible to provide a unique Name for this identity, consider setting the static property AntiForgeryConfig.AdditionalDataProvider to an instance of a type that can provide some form of unique identifier for the current user.. + /// </summary> + internal static string TokenValidator_AuthenticatedUserWithoutUsername { + get { + return ResourceManager.GetString("TokenValidator_AuthenticatedUserWithoutUsername", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Validation parameter names in unobtrusive client validation rules cannot be empty. Client rule type: {0}. /// </summary> internal static string UnobtrusiveJavascript_ValidationParameterCannotBeEmpty { diff --git a/src/System.Web.WebPages/Resources/WebPageResources.resx b/src/System.Web.WebPages/Resources/WebPageResources.resx index e88eb921..958d9c4b 100644 --- a/src/System.Web.WebPages/Resources/WebPageResources.resx +++ b/src/System.Web.WebPages/Resources/WebPageResources.resx @@ -117,8 +117,32 @@ <resheader name="writer"> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> - <data name="AntiForgeryToken_ValidationFailed" xml:space="preserve"> - <value>A required anti-forgery token was not supplied or was invalid.</value> + <data name="AntiForgeryToken_AdditionalDataCheckFailed" xml:space="preserve"> + <value>The provided anti-forgery token failed a custom data check.</value> + </data> + <data name="AntiForgeryToken_ClaimUidMismatch" xml:space="preserve"> + <value>The provided anti-forgery token was meant for a different claims-based user than the current user.</value> + </data> + <data name="AntiForgeryToken_CookieMissing" xml:space="preserve"> + <value>The required anti-forgery cookie "{0}" is not present.</value> + </data> + <data name="AntiForgeryToken_DeserializationFailed" xml:space="preserve"> + <value>The anti-forgery token could not be decrypted. If this application is hosted by a Web Farm or cluster, ensure that all machines are running the same version of ASP.NET Web Pages and that the <machineKey> configuration specifies explicit encryption and validation keys. AutoGenerate cannot be used in a cluster.</value> + </data> + <data name="AntiForgeryToken_FormFieldMissing" xml:space="preserve"> + <value>The required anti-forgery form field "{0}" is not present.</value> + </data> + <data name="AntiForgeryToken_SecurityTokenMismatch" xml:space="preserve"> + <value>The anti-forgery cookie token and form field token do not match.</value> + </data> + <data name="AntiForgeryToken_TokensSwapped" xml:space="preserve"> + <value>Validation of the provided anti-forgery token failed. The cookie "{0}" and the form field "{1}" were swapped.</value> + </data> + <data name="AntiForgeryToken_UsernameMismatch" xml:space="preserve"> + <value>The provided anti-forgery token was meant for user "{0}", but the current user is "{1}".</value> + </data> + <data name="AntiForgeryWorker_RequireSSL" xml:space="preserve"> + <value>The anti-forgery system has the configuration value AntiForgeryConfig.RequireSsl = true, but the current request is not an SSL request.</value> </data> <data name="ApplicationPart_ModuleAlreadyRegistered" xml:space="preserve"> <value>The assembly "{0}" is already registered.</value> @@ -135,6 +159,12 @@ <data name="ApplicationPart_ResourceNotFound" xml:space="preserve"> <value>The resource file "{0}" could not be found.</value> </data> + <data name="ClaimUidExtractor_ClaimNotPresent" xml:space="preserve"> + <value>A claim of type '{0}' was not present on the provided ClaimsIdentity.</value> + </data> + <data name="ClaimUidExtractor_DefaultClaimsNotPresent" xml:space="preserve"> + <value>A claim of type 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier' or 'http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider' was not present on the provided ClaimsIdentity. To enable anti-forgery token support with claims-based authentication, please verify that the configured claims provider is providing both of these claims on the ClaimsIdentity instances it generates. If the configured claims provider instead uses a different claim type as a unique identifier, it can be configured by setting the static property AntiForgeryConfig.UniqueClaimTypeIdentifier.</value> + </data> <data name="DynamicDictionary_InvalidNumberOfIndexes" xml:space="preserve"> <value>Index length must be exactly one.</value> </data> @@ -168,6 +198,9 @@ <data name="StateStorage_StorageScopesCannotBeCreated" xml:space="preserve"> <value>Storage scopes cannot be created when _AppStart is executing.</value> </data> + <data name="TokenValidator_AuthenticatedUserWithoutUsername" xml:space="preserve"> + <value>The provided identity of type '{0}' is marked IsAuthenticated = true but does not have a value for Name. By default, the anti-forgery system requires that all authenticated identities have a unique Name. If it is not possible to provide a unique Name for this identity, consider setting the static property AntiForgeryConfig.AdditionalDataProvider to an instance of a type that can provide some form of unique identifier for the current user.</value> + </data> <data name="UnobtrusiveJavascript_ValidationParameterCannotBeEmpty" xml:space="preserve"> <value>Validation parameter names in unobtrusive client validation rules cannot be empty. Client rule type: {0}</value> </data> diff --git a/src/System.Web.WebPages/System.Web.WebPages.csproj b/src/System.Web.WebPages/System.Web.WebPages.csproj index 571e2c87..2990917f 100644 --- a/src/System.Web.WebPages/System.Web.WebPages.csproj +++ b/src/System.Web.WebPages/System.Web.WebPages.csproj @@ -101,13 +101,31 @@ <Compile Include="DisplayInfo.cs" /> <Compile Include="DisplayModeProvider.cs" /> <Compile Include="BrowserHelpers.cs" /> + <Compile Include="Helpers\AntiXsrf\ClaimUidExtractor.cs" /> + <Compile Include="Helpers\AntiXsrf\AntiForgeryTokenStore.cs" /> + <Compile Include="Helpers\AntiXsrf\ITokenStore.cs" /> + <Compile Include="Helpers\AntiXsrf\TokenValidator.cs" /> + <Compile Include="Helpers\AntiXsrf\ITokenValidator.cs" /> + <Compile Include="Helpers\AntiXsrf\IClaimUidExtractor.cs" /> + <Compile Include="Helpers\AntiXsrf\MachineKeyCryptoSystem.cs" /> + <Compile Include="Helpers\AntiXsrf\ICryptoSystem.cs" /> + <Compile Include="Helpers\Claims\Claim.cs" /> + <Compile Include="Helpers\Claims\ClaimsIdentity.cs" /> + <Compile Include="Helpers\Claims\ClaimsIdentityConverter.cs" /> + <Compile Include="Helpers\AntiXsrf\AntiForgeryConfigWrapper.cs" /> + <Compile Include="Helpers\AntiXsrf\AntiForgeryWorker.cs" /> + <Compile Include="Helpers\AntiXsrf\IAntiForgeryTokenSerializer.cs" /> + <Compile Include="Helpers\AntiXsrf\IAntiForgeryConfig.cs" /> + <Compile Include="Helpers\CryptoUtil.cs" /> + <Compile Include="Helpers\AntiXsrf\AntiForgeryToken.cs" /> + <Compile Include="Helpers\AntiXsrf\BinaryBlob.cs" /> + <Compile Include="Helpers\AntiXsrf\AntiForgeryTokenSerializer.cs" /> + <Compile Include="Helpers\AntiForgeryConfig.cs" /> + <Compile Include="Helpers\IAntiForgeryAdditionalDataProvider.cs" /> <Compile Include="IDisplayMode.cs" /> <Compile Include="DefaultDisplayMode.cs" /> <Compile Include="FileExistenceCache.cs" /> - <Compile Include="Helpers\AntiForgeryData.cs" /> - <Compile Include="Helpers\AntiForgeryDataSerializer.cs" /> <Compile Include="Helpers\AntiForgery.cs" /> - <Compile Include="Helpers\AntiForgeryWorker.cs" /> <Compile Include="HttpContextExtensions.cs" /> <Compile Include="Instrumentation\HttpContextAdapter.Availability.cs" /> <Compile Include="Instrumentation\HttpContextAdapter.generated.cs"> diff --git a/test/System.Web.Mvc.Test/Test/ValidateAntiForgeryTokenAttributeTest.cs b/test/System.Web.Mvc.Test/Test/ValidateAntiForgeryTokenAttributeTest.cs index 0b9464cf..7dfb5258 100644 --- a/test/System.Web.Mvc.Test/Test/ValidateAntiForgeryTokenAttributeTest.cs +++ b/test/System.Web.Mvc.Test/Test/ValidateAntiForgeryTokenAttributeTest.cs @@ -27,16 +27,11 @@ namespace System.Web.Mvc.Test Mock<AuthorizationContext> authorizationContextMock = new Mock<AuthorizationContext>(); authorizationContextMock.SetupGet(ac => ac.HttpContext).Returns(context); bool validateCalled = false; - Action<HttpContextBase, string> validateMethod = (c, s) => + Action validateMethod = () => { - Assert.Same(context, c); - Assert.Equal("some salt", s); validateCalled = true; }; - ValidateAntiForgeryTokenAttribute attribute = new ValidateAntiForgeryTokenAttribute(validateMethod) - { - Salt = "some salt" - }; + ValidateAntiForgeryTokenAttribute attribute = new ValidateAntiForgeryTokenAttribute(validateMethod); // Act attribute.OnAuthorization(authorizationContextMock.Object); @@ -46,16 +41,6 @@ namespace System.Web.Mvc.Test } [Fact] - public void SaltProperty() - { - // Arrange - ValidateAntiForgeryTokenAttribute attribute = new ValidateAntiForgeryTokenAttribute(); - - // Act & Assert - MemberHelper.TestStringProperty(attribute, "Salt", String.Empty); - } - - [Fact] public void ValidateThunk_DefaultsToAntiForgeryMethod() { // Arrange diff --git a/test/System.Web.WebPages.Test/Helpers/AntiForgeryConfigTest.cs b/test/System.Web.WebPages.Test/Helpers/AntiForgeryConfigTest.cs new file mode 100644 index 00000000..2d05bc13 --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/AntiForgeryConfigTest.cs @@ -0,0 +1,22 @@ +using Xunit.Extensions; +using Assert = Microsoft.TestCommon.AssertEx; + +namespace System.Web.Helpers.Test +{ + public class AntiForgeryConfigTest + { + [Theory] + [InlineData(null, "__RequestVerificationToken")] + [InlineData("", "__RequestVerificationToken")] + [InlineData("/", "__RequestVerificationToken")] + [InlineData("/path", "__RequestVerificationToken_L3BhdGg1")] + public void GetAntiForgeryCookieName(string appPath, string expectedCookieName) + { + // Act + string retVal = AntiForgeryConfig.GetAntiForgeryCookieName(appPath); + + // Assert + Assert.Equal(expectedCookieName, retVal); + } + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/AntiForgeryDataSerializerTest.cs b/test/System.Web.WebPages.Test/Helpers/AntiForgeryDataSerializerTest.cs deleted file mode 100644 index 09b9af22..00000000 --- a/test/System.Web.WebPages.Test/Helpers/AntiForgeryDataSerializerTest.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Web.Mvc; -using Xunit; -using Assert = Microsoft.TestCommon.AssertEx; - -namespace System.Web.Helpers.Test -{ - public class AntiForgeryDataSerializerTest - { - [Fact] - public void GuardClauses() - { - // Arrange - AntiForgeryDataSerializer serializer = new AntiForgeryDataSerializer(); - - // Act & assert - Assert.ThrowsArgumentNull( - () => serializer.Serialize(null), - "token" - ); - Assert.ThrowsArgumentNullOrEmptyString( - () => serializer.Deserialize(null), - "serializedToken" - ); - Assert.ThrowsArgumentNullOrEmptyString( - () => serializer.Deserialize(String.Empty), - "serializedToken" - ); - Assert.Throws<HttpAntiForgeryException>( - () => serializer.Deserialize("Corrupted Base-64 Value"), - "A required anti-forgery token was not supplied or was invalid." - ); - } - - [Fact] - public void DeserializationExceptionDoesNotContainInnerException() - { - // Arrange - AntiForgeryDataSerializer serializer = new AntiForgeryDataSerializer(); - - // Act & assert - HttpAntiForgeryException exception = null; - try - { - serializer.Deserialize("Can't deserialize this."); - } - catch (HttpAntiForgeryException ex) - { - exception = ex; - } - - Assert.NotNull(exception); - Assert.Null(exception.InnerException); - } - - [Fact] - public void CanRoundTripData() - { - // Arrange - AntiForgeryDataSerializer serializer = new AntiForgeryDataSerializer - { - Decoder = value => Convert.FromBase64String(value), - Encoder = bytes => Convert.ToBase64String(bytes), - }; - AntiForgeryData input = new AntiForgeryData - { - Salt = "The Salt", - Username = "The Username", - Value = "The Value", - CreationDate = DateTime.Now, - }; - - // Act - AntiForgeryData output = serializer.Deserialize(serializer.Serialize(input)); - - // Assert - Assert.NotNull(output); - Assert.Equal(input.Salt, output.Salt); - Assert.Equal(input.Username, output.Username); - Assert.Equal(input.Value, output.Value); - Assert.Equal(input.CreationDate, output.CreationDate); - } - - [Fact] - public void HexDigitConvertsIntegersToHexCharsCorrectly() - { - for (int i = 0; i < 0x10; i++) - { - Assert.Equal(i.ToString("X")[0], AntiForgeryDataSerializer.HexDigit(i)); - } - } - - [Fact] - public void HexValueConvertsCharValuesToIntegersCorrectly() - { - for (int i = 0; i < 0x10; i++) - { - var hexChar = i.ToString("X")[0]; - Assert.Equal(i, AntiForgeryDataSerializer.HexValue(hexChar)); - } - } - } -} diff --git a/test/System.Web.WebPages.Test/Helpers/AntiForgeryDataTest.cs b/test/System.Web.WebPages.Test/Helpers/AntiForgeryDataTest.cs deleted file mode 100644 index 442b0c9f..00000000 --- a/test/System.Web.WebPages.Test/Helpers/AntiForgeryDataTest.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System.Security.Principal; -using Moq; -using Xunit; -using Assert = Microsoft.TestCommon.AssertEx; - -namespace System.Web.Helpers.Test -{ - public class AntiForgeryDataTest - { - [Fact] - public void CopyConstructor() - { - // Arrange - AntiForgeryData originalToken = new AntiForgeryData() - { - CreationDate = DateTime.Now, - Salt = "some salt", - Value = "some value" - }; - - // Act - AntiForgeryData newToken = new AntiForgeryData(originalToken); - - // Assert - Assert.Equal(originalToken.CreationDate, newToken.CreationDate); - Assert.Equal(originalToken.Salt, newToken.Salt); - Assert.Equal(originalToken.Value, newToken.Value); - } - - [Fact] - public void CopyConstructorThrowsIfTokenIsNull() - { - // Act & Assert - Assert.ThrowsArgumentNull( - delegate { new AntiForgeryData(null); }, "token"); - } - - [Fact] - public void CreationDateProperty() - { - // Arrange - AntiForgeryData token = new AntiForgeryData(); - - // Act & Assert - var now = DateTime.UtcNow; - token.CreationDate = now; - Assert.Equal(now, token.CreationDate); - } - - [Fact] - public void GetAntiForgeryTokenNameReturnsEncodedCookieNameIfAppPathIsNotEmpty() - { - // Arrange - // the string below (as UTF-8 bytes) base64-encodes to "Pz4/Pj8+Pz4/Pj8+Pz4/Pg==" - string original = "?>?>?>?>?>?>?>?>"; - - // Act - string tokenName = AntiForgeryData.GetAntiForgeryTokenName(original); - - // Assert - Assert.Equal("__RequestVerificationToken_Pz4-Pj8.Pz4-Pj8.Pz4-Pg__", tokenName); - } - - [Fact] - public void GetAntiForgeryTokenNameReturnsFieldNameIfAppPathIsNull() - { - // Act - string tokenName = AntiForgeryData.GetAntiForgeryTokenName(null); - - // Assert - Assert.Equal("__RequestVerificationToken", tokenName); - } - - [Fact] - public void GetUsername_ReturnsEmptyStringIfIdentityIsNull() - { - // Arrange - Mock<IPrincipal> mockPrincipal = new Mock<IPrincipal>(); - mockPrincipal.Setup(o => o.Identity).Returns((IIdentity)null); - - // Act - string username = AntiForgeryData.GetUsername(mockPrincipal.Object); - - // Assert - Assert.Equal("", username); - } - - [Fact] - public void GetUsername_ReturnsEmptyStringIfPrincipalIsNull() - { - // Act - string username = AntiForgeryData.GetUsername(null); - - // Assert - Assert.Equal("", username); - } - - [Fact] - public void GetUsername_ReturnsEmptyStringIfUserNotAuthenticated() - { - // Arrange - Mock<IPrincipal> mockPrincipal = new Mock<IPrincipal>(); - mockPrincipal.Setup(o => o.Identity.IsAuthenticated).Returns(false); - mockPrincipal.Setup(o => o.Identity.Name).Returns("SampleName"); - - // Act - string username = AntiForgeryData.GetUsername(mockPrincipal.Object); - - // Assert - Assert.Equal("", username); - } - - [Fact] - public void GetUsername_ReturnsUsernameIfUserIsAuthenticated() - { - // Arrange - Mock<IPrincipal> mockPrincipal = new Mock<IPrincipal>(); - mockPrincipal.Setup(o => o.Identity.IsAuthenticated).Returns(true); - mockPrincipal.Setup(o => o.Identity.Name).Returns("SampleName"); - - // Act - string username = AntiForgeryData.GetUsername(mockPrincipal.Object); - - // Assert - Assert.Equal("SampleName", username); - } - - [Fact] - public void NewToken() - { - // Act - AntiForgeryData token = AntiForgeryData.NewToken(); - - // Assert - int valueLength = Convert.FromBase64String(token.Value).Length; - Assert.Equal(16, valueLength); - Assert.NotEqual(default(DateTime), token.CreationDate); - } - - [Fact] - public void SaltProperty() - { - // Arrange - AntiForgeryData token = new AntiForgeryData(); - - // Act & Assert - Assert.Equal(String.Empty, token.Salt); - token.Salt = null; - Assert.Equal(String.Empty, token.Salt); - token.Salt = String.Empty; - Assert.Equal(String.Empty, token.Salt); - } - - [Fact] - public void ValueProperty() - { - // Arrange - AntiForgeryData token = new AntiForgeryData(); - - // Act & Assert - Assert.Equal(String.Empty, token.Value); - token.Value = null; - Assert.Equal(String.Empty, token.Value); - token.Value = String.Empty; - Assert.Equal(String.Empty, token.Value); - } - } -} diff --git a/test/System.Web.WebPages.Test/Helpers/AntiForgeryTest.cs b/test/System.Web.WebPages.Test/Helpers/AntiForgeryTest.cs index db26de50..d1d89147 100644 --- a/test/System.Web.WebPages.Test/Helpers/AntiForgeryTest.cs +++ b/test/System.Web.WebPages.Test/Helpers/AntiForgeryTest.cs @@ -5,8 +5,6 @@ namespace System.Web.Helpers.Test { public class AntiForgeryTest { - private static string _antiForgeryTokenCookieName = AntiForgeryData.GetAntiForgeryTokenName("/SomeAppPath"); - [Fact] public void GetHtml_ThrowsWhenNotCalledInWebContext() { @@ -15,9 +13,10 @@ namespace System.Web.Helpers.Test } [Fact] - public void GetHtml_ThrowsOnNullContext() + public void GetTokens_ThrowsWhenNotCalledInWebContext() { - Assert.ThrowsArgumentNull(() => AntiForgery.GetHtml(null, null, null, null), "httpContext"); + Assert.Throws<ArgumentException>(() => { string dummy1, dummy2; AntiForgery.GetTokens("dummy", out dummy1, out dummy2); }, + "An HttpContext is required to perform this operation. Check that this operation is being performed during a web request."); } [Fact] @@ -25,12 +24,9 @@ namespace System.Web.Helpers.Test { Assert.Throws<ArgumentException>(() => AntiForgery.Validate(), "An HttpContext is required to perform this operation. Check that this operation is being performed during a web request."); - } - [Fact] - public void Validate_ThrowsOnNullContext() - { - Assert.ThrowsArgumentNull(() => AntiForgery.Validate(httpContext: null, salt: null), "httpContext"); + Assert.Throws<ArgumentException>(() => AntiForgery.Validate("cookie-token", "form-token"), + "An HttpContext is required to perform this operation. Check that this operation is being performed during a web request."); } } } diff --git a/test/System.Web.WebPages.Test/Helpers/AntiForgeryWorkerTest.cs b/test/System.Web.WebPages.Test/Helpers/AntiForgeryWorkerTest.cs deleted file mode 100644 index 81cd287d..00000000 --- a/test/System.Web.WebPages.Test/Helpers/AntiForgeryWorkerTest.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System.Collections.Specialized; -using System.Globalization; -using System.Text.RegularExpressions; -using System.Web.Mvc; -using Moq; -using Xunit; -using Assert = Microsoft.TestCommon.AssertEx; -using Match = System.Text.RegularExpressions.Match; - -namespace System.Web.Helpers.Test -{ - public class AntiForgeryWorkerTest - { - private static string _antiForgeryTokenCookieName = AntiForgeryData.GetAntiForgeryTokenName("/SomeAppPath"); - private const string _serializedValuePrefix = @"<input name=""__RequestVerificationToken"" type=""hidden"" value=""Creation: "; - private const string _someValueSuffix = @", Value: some value, Salt: some other salt, Username: username"" />"; - private readonly Regex _randomFormValueSuffixRegex = new Regex(@", Value: (?<value>[A-Za-z0-9/\+=]{24}), Salt: some other salt, Username: username"" />$"); - private readonly Regex _randomCookieValueSuffixRegex = new Regex(@", Value: (?<value>[A-Za-z0-9/\+=]{24}), Salt: "); - - [Fact] - public void Serializer_DefaultValueIsAntiForgeryDataSerializer() - { - Assert.Same(typeof(AntiForgeryDataSerializer), new AntiForgeryWorker().Serializer.GetType()); - } - - [Fact] - public void GetHtml_ReturnsFormFieldAndSetsCookieValueIfDoesNotExist() - { - // Arrange - AntiForgeryWorker worker = new AntiForgeryWorker() - { - Serializer = new DummyAntiForgeryTokenSerializer() - }; - var context = CreateContext(); - - // Act - string formValue = worker.GetHtml(context, "some other salt", null, null).ToHtmlString(); - - // Assert - Assert.True(formValue.StartsWith(_serializedValuePrefix), "Form value prefix did not match."); - - Match formMatch = _randomFormValueSuffixRegex.Match(formValue); - string formTokenValue = formMatch.Groups["value"].Value; - - HttpCookie cookie = context.Response.Cookies[_antiForgeryTokenCookieName]; - Assert.NotNull(cookie); - Assert.True(cookie.HttpOnly, "Cookie should have HTTP-only flag set."); - Assert.True(String.IsNullOrEmpty(cookie.Domain), "Domain should not have been set."); - Assert.Equal("/", cookie.Path); - - Match cookieMatch = _randomCookieValueSuffixRegex.Match(cookie.Value); - string cookieTokenValue = cookieMatch.Groups["value"].Value; - - Assert.Equal(formTokenValue, cookieTokenValue); - } - - [Fact] - public void GetHtml_SetsCookieDomainAndPathIfSpecified() - { - // Arrange - AntiForgeryWorker worker = new AntiForgeryWorker() - { - Serializer = new DummyAntiForgeryTokenSerializer() - }; - var context = CreateContext(); - - // Act - string formValue = worker.GetHtml(context, "some other salt", "theDomain", "thePath").ToHtmlString(); - - // Assert - Assert.True(formValue.StartsWith(_serializedValuePrefix), "Form value prefix did not match."); - - Match formMatch = _randomFormValueSuffixRegex.Match(formValue); - string formTokenValue = formMatch.Groups["value"].Value; - - HttpCookie cookie = context.Response.Cookies[_antiForgeryTokenCookieName]; - Assert.NotNull(cookie); - Assert.True(cookie.HttpOnly, "Cookie should have HTTP-only flag set."); - Assert.Equal("theDomain", cookie.Domain); - Assert.Equal("thePath", cookie.Path); - - Match cookieMatch = _randomCookieValueSuffixRegex.Match(cookie.Value); - string cookieTokenValue = cookieMatch.Groups["value"].Value; - - Assert.Equal(formTokenValue, cookieTokenValue); - } - - [Fact] - public void GetHtml_ReusesCookieValueIfExistsAndIsValid() - { - // Arrange - AntiForgeryWorker worker = new AntiForgeryWorker() - { - Serializer = new DummyAntiForgeryTokenSerializer() - }; - var context = CreateContext("2001-01-01:some value:some salt:username"); - - // Act - string formValue = worker.GetHtml(context, "some other salt", null, null).ToHtmlString(); - - // Assert - Assert.True(formValue.StartsWith(_serializedValuePrefix), "Form value prefix did not match."); - Assert.True(formValue.EndsWith(_someValueSuffix), "Form value suffix did not match."); - Assert.Equal(0, context.Response.Cookies.Count); - } - - [Fact] - public void GetHtml_CreatesNewCookieValueIfCookieExistsButIsNotValid() - { - // Arrange - AntiForgeryWorker worker = new AntiForgeryWorker() - { - Serializer = new DummyAntiForgeryTokenSerializer() - }; - var context = CreateContext("invalid"); - - // Act - string formValue = worker.GetHtml(context, "some other salt", null, null).ToHtmlString(); - - // Assert - Assert.True(formValue.StartsWith(_serializedValuePrefix), "Form value prefix did not match."); - - Match formMatch = _randomFormValueSuffixRegex.Match(formValue); - string formTokenValue = formMatch.Groups["value"].Value; - - HttpCookie cookie = context.Response.Cookies[_antiForgeryTokenCookieName]; - Assert.NotNull(cookie); - Assert.True(cookie.HttpOnly, "Cookie should have HTTP-only flag set."); - Assert.True(String.IsNullOrEmpty(cookie.Domain), "Domain should not have been set."); - Assert.Equal("/", cookie.Path); - - Match cookieMatch = _randomCookieValueSuffixRegex.Match(cookie.Value); - string cookieTokenValue = cookieMatch.Groups["value"].Value; - - Assert.Equal(formTokenValue, cookieTokenValue); - } - - [Fact] - public void Validate_ThrowsIfCookieMissing() - { - Validate_Helper(null, "2001-01-01:some other value:the real salt:username"); - } - - [Fact] - public void Validate_ThrowsIfCookieValueDoesNotMatchFormValue() - { - Validate_Helper("2001-01-01:some value:the real salt:username", "2001-01-01:some other value:the real salt:username"); - } - - [Fact] - public void Validate_ThrowsIfFormSaltDoesNotMatchAttributeSalt() - { - Validate_Helper("2001-01-01:some value:some salt:username", "2001-01-01:some value:some other salt:username"); - } - - [Fact] - public void Validate_ThrowsIfFormValueMissing() - { - Validate_Helper("2001-01-01:some value:the real salt:username", null); - } - - [Fact] - public void Validate_ThrowsIfUsernameInFormIsIncorrect() - { - Validate_Helper("2001-01-01:value:salt:username", "2001-01-01:value:salt:different username"); - } - - private static void Validate_Helper(string cookieValue, string formValue, string username = "username") - { - // Arrange - //ValidateAntiForgeryTokenAttribute attribute = GetAttribute(); - var context = CreateContext(cookieValue, formValue, username); - - AntiForgeryWorker worker = new AntiForgeryWorker() - { - Serializer = new DummyAntiForgeryTokenSerializer() - }; - - // Act & Assert - Assert.Throws<HttpAntiForgeryException>( - delegate - { - //attribute.OnAuthorization(authContext); - worker.Validate(context, "the real salt"); - }, "A required anti-forgery token was not supplied or was invalid."); - } - - private static HttpContextBase CreateContext(string cookieValue = null, string formValue = null, string username = "username") - { - HttpCookieCollection requestCookies = new HttpCookieCollection(); - if (!String.IsNullOrEmpty(cookieValue)) - { - requestCookies.Set(new HttpCookie(_antiForgeryTokenCookieName, cookieValue)); - } - NameValueCollection formCollection = new NameValueCollection(); - if (!String.IsNullOrEmpty(formValue)) - { - formCollection.Set(AntiForgeryData.GetAntiForgeryTokenName(null), formValue); - } - - Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>(); - mockContext.Setup(c => c.Request.ApplicationPath).Returns("/SomeAppPath"); - mockContext.Setup(c => c.Request.Cookies).Returns(requestCookies); - mockContext.Setup(c => c.Request.Form).Returns(formCollection); - mockContext.Setup(c => c.Response.Cookies).Returns(new HttpCookieCollection()); - mockContext.Setup(c => c.User.Identity.IsAuthenticated).Returns(true); - mockContext.Setup(c => c.User.Identity.Name).Returns(username); - - return mockContext.Object; - } - - internal class DummyAntiForgeryTokenSerializer : AntiForgeryDataSerializer - { - public override string Serialize(AntiForgeryData token) - { - return String.Format(CultureInfo.InvariantCulture, "Creation: {0}, Value: {1}, Salt: {2}, Username: {3}", - token.CreationDate, token.Value, token.Salt, token.Username); - } - - public override AntiForgeryData Deserialize(string serializedToken) - { - if (serializedToken == "invalid") - { - throw new HttpAntiForgeryException(); - } - string[] parts = serializedToken.Split(':'); - return new AntiForgeryData() - { - CreationDate = DateTime.Parse(parts[0], CultureInfo.InvariantCulture), - Value = parts[1], - Salt = parts[2], - Username = parts[3] - }; - } - } - } -} diff --git a/test/System.Web.WebPages.Test/Helpers/AntiXsrf/AntiForgeryTokenSerializerTest.cs b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/AntiForgeryTokenSerializerTest.cs new file mode 100644 index 00000000..4917a78f --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/AntiForgeryTokenSerializerTest.cs @@ -0,0 +1,176 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.Web.Mvc; +using Moq; +using Xunit; +using Xunit.Extensions; +using Assert = Microsoft.TestCommon.AssertEx; + +namespace System.Web.Helpers.AntiXsrf.Test +{ + public class AntiForgeryTokenSerializerTest + { + private static readonly AntiForgeryTokenSerializer _testSerializer = new AntiForgeryTokenSerializer(cryptoSystem: CreateIdentityTransformCryptoSystem()); + + private static readonly BinaryBlob _claimUid = new BinaryBlob(256, new byte[] { 0x6F, 0x16, 0x48, 0xE9, 0x72, 0x49, 0xAA, 0x58, 0x75, 0x40, 0x36, 0xA6, 0x7E, 0x24, 0x8C, 0xF0, 0x44, 0xF0, 0x7E, 0xCF, 0xB0, 0xED, 0x38, 0x75, 0x56, 0xCE, 0x02, 0x9A, 0x4F, 0x9A, 0x40, 0xE0 }); + private static readonly BinaryBlob _securityToken = new BinaryBlob(128, new byte[] { 0x70, 0x5E, 0xED, 0xCC, 0x7D, 0x42, 0xF1, 0xD6, 0xB3, 0xB9, 0x8A, 0x59, 0x36, 0x25, 0xBB, 0x4C }); + + [Theory] + [InlineData( + "01" // Version + + "705EEDCC7D42F1D6B3B9" // SecurityToken + // (WRONG!) Stream ends too early + )] + [InlineData( + "01" // Version + + "705EEDCC7D42F1D6B3B98A593625BB4C" // SecurityToken + + "01" // IsSessionToken + + "00" // (WRONG!) Too much data in stream + )] + [InlineData( + "02" // (WRONG! - must be 0x01) Version + + "705EEDCC7D42F1D6B3B98A593625BB4C" // SecurityToken + + "01" // IsSessionToken + )] + [InlineData( + "01" // Version + + "705EEDCC7D42F1D6B3B98A593625BB4C" // SecurityToken + + "00" // IsSessionToken + + "00" // IsClaimsBased + + "05" // Username length header + + "0000" // (WRONG!) Too little data in stream + )] + public void Deserialize_BadToken(string serializedToken) + { + // Act & assert + var ex = Assert.Throws<HttpAntiForgeryException>(() => _testSerializer.Deserialize(serializedToken)); + Assert.Equal(@"The anti-forgery token could not be decrypted. If this application is hosted by a Web Farm or cluster, ensure that all machines are running the same version of ASP.NET Web Pages and that the <machineKey> configuration specifies explicit encryption and validation keys. AutoGenerate cannot be used in a cluster.", ex.Message); + } + + [Fact] + public void Serialize_FieldToken_WithClaimUid() + { + // Arrange + const string expectedSerializedData = + "01" // Version + + "705EEDCC7D42F1D6B3B98A593625BB4C" // SecurityToken + + "00" // IsSessionToken + + "01" // IsClaimsBased + + "6F1648E97249AA58754036A67E248CF044F07ECFB0ED387556CE029A4F9A40E0" // ClaimUid + + "05" // AdditionalData length header + + "E282AC3437"; // AdditionalData ("€47") as UTF8 + + AntiForgeryToken token = new AntiForgeryToken() + { + SecurityToken = _securityToken, + IsSessionToken = false, + ClaimUid = _claimUid, + AdditionalData = "€47" + }; + + // Act & assert - serialization + string actualSerializedData = _testSerializer.Serialize(token); + Assert.Equal(expectedSerializedData, actualSerializedData); + + // Act & assert - deserialization + AntiForgeryToken deserializedToken = _testSerializer.Deserialize(actualSerializedData); + AssertTokensEqual(token, deserializedToken); + } + + [Fact] + public void Serialize_FieldToken_WithUsername() + { + // Arrange + const string expectedSerializedData = + "01" // Version + + "705EEDCC7D42F1D6B3B98A593625BB4C" // SecurityToken + + "00" // IsSessionToken + + "00" // IsClaimsBased + + "08" // Username length header + + "4AC3A972C3B46D65" // Username ("Jérôme") as UTF8 + + "05" // AdditionalData length header + + "E282AC3437"; // AdditionalData ("€47") as UTF8 + + AntiForgeryToken token = new AntiForgeryToken() + { + SecurityToken = _securityToken, + IsSessionToken = false, + Username = "Jérôme", + AdditionalData = "€47" + }; + + // Act & assert - serialization + string actualSerializedData = _testSerializer.Serialize(token); + Assert.Equal(expectedSerializedData, actualSerializedData); + + // Act & assert - deserialization + AntiForgeryToken deserializedToken = _testSerializer.Deserialize(actualSerializedData); + AssertTokensEqual(token, deserializedToken); + } + + [Fact] + public void Serialize_SessionToken() + { + // Arrange + const string expectedSerializedData = + "01" // Version + + "705EEDCC7D42F1D6B3B98A593625BB4C" // SecurityToken + + "01"; // IsSessionToken + + AntiForgeryToken token = new AntiForgeryToken() + { + SecurityToken = _securityToken, + IsSessionToken = true + }; + + // Act & assert - serialization + string actualSerializedData = _testSerializer.Serialize(token); + Assert.Equal(expectedSerializedData, actualSerializedData); + + // Act & assert - deserialization + AntiForgeryToken deserializedToken = _testSerializer.Deserialize(actualSerializedData); + AssertTokensEqual(token, deserializedToken); + } + + private static string BytesToHex(byte[] bytes) + { + StringBuilder sb = new StringBuilder(); + foreach (byte b in bytes) + { + sb.AppendFormat(CultureInfo.InvariantCulture, "{0:X2}", b); + } + return sb.ToString(); + } + + private static byte[] HexToBytes(string hex) + { + List<byte> bytes = new List<byte>(); + for (int i = 0; i < hex.Length; i += 2) + { + byte b = Byte.Parse(hex.Substring(i, 2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture); + bytes.Add(b); + } + return bytes.ToArray(); + } + + private static ICryptoSystem CreateIdentityTransformCryptoSystem() + { + Mock<MockableCryptoSystem> mockCryptoSystem = new Mock<MockableCryptoSystem>(); + mockCryptoSystem.Setup(o => o.Protect(It.IsAny<byte[]>())).Returns<byte[]>(HexUtil.HexEncode); + mockCryptoSystem.Setup(o => o.Unprotect(It.IsAny<string>())).Returns<string>(HexUtil.HexDecode); + return mockCryptoSystem.Object; + } + + private static void AssertTokensEqual(AntiForgeryToken expected, AntiForgeryToken actual) + { + Assert.NotNull(expected); + Assert.NotNull(actual); + Assert.Equal(expected.AdditionalData, actual.AdditionalData); + Assert.Equal(expected.ClaimUid, actual.ClaimUid); + Assert.Equal(expected.IsSessionToken, actual.IsSessionToken); + Assert.Equal(expected.SecurityToken, actual.SecurityToken); + Assert.Equal(expected.Username, actual.Username); + } + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/AntiXsrf/AntiForgeryTokenStoreTest.cs b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/AntiForgeryTokenStoreTest.cs new file mode 100644 index 00000000..98a00ccc --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/AntiForgeryTokenStoreTest.cs @@ -0,0 +1,234 @@ +using System.Web.Mvc; +using Moq; +using Xunit; +using Xunit.Extensions; +using Assert = Microsoft.TestCommon.AssertEx; + +namespace System.Web.Helpers.AntiXsrf.Test +{ + public class AntiForgeryTokenStoreTest + { + [Fact] + public void GetCookieToken_CookieDoesNotExist_ReturnsNull() + { + // Arrange + Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(); + mockHttpContext.Setup(o => o.Request.Cookies).Returns(new HttpCookieCollection()); + + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + CookieName = "cookie-name" + }; + + AntiForgeryTokenStore tokenStore = new AntiForgeryTokenStore( + config: config, + serializer: null); + + // Act + AntiForgeryToken token = tokenStore.GetCookieToken(mockHttpContext.Object); + + // Assert + Assert.Null(token); + } + + [Fact] + public void GetCookieToken_CookieIsEmpty_ReturnsNull() + { + // Arrange + Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(); + mockHttpContext.Setup(o => o.Request.Cookies).Returns(new HttpCookieCollection() + { + new HttpCookie("cookie-name", "") + }); + + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + CookieName = "cookie-name" + }; + + AntiForgeryTokenStore tokenStore = new AntiForgeryTokenStore( + config: config, + serializer: null); + + // Act + AntiForgeryToken token = tokenStore.GetCookieToken(mockHttpContext.Object); + + // Assert + Assert.Null(token); + } + + [Fact] + public void GetCookieToken_CookieIsInvalid_PropagatesException() + { + // Arrange + Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(); + mockHttpContext.Setup(o => o.Request.Cookies).Returns(new HttpCookieCollection() + { + new HttpCookie("cookie-name", "invalid-value") + }); + + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + CookieName = "cookie-name" + }; + + HttpAntiForgeryException expectedException = new HttpAntiForgeryException("some exception"); + Mock<MockableAntiForgeryTokenSerializer> mockSerializer = new Mock<MockableAntiForgeryTokenSerializer>(); + mockSerializer.Setup(o => o.Deserialize("invalid-value")).Throws(expectedException); + + AntiForgeryTokenStore tokenStore = new AntiForgeryTokenStore( + config: config, + serializer: mockSerializer.Object); + + // Act & assert + var ex = Assert.Throws<HttpAntiForgeryException>(() => tokenStore.GetCookieToken(mockHttpContext.Object)); + Assert.Equal(expectedException, ex); + } + + [Fact] + public void GetCookieToken_CookieIsValid_ReturnsToken() + { + // Arrange + AntiForgeryToken expectedToken = new AntiForgeryToken(); + + Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(); + mockHttpContext.Setup(o => o.Request.Cookies).Returns(new HttpCookieCollection() + { + new HttpCookie("cookie-name", "valid-value") + }); + + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + CookieName = "cookie-name" + }; + + Mock<MockableAntiForgeryTokenSerializer> mockSerializer = new Mock<MockableAntiForgeryTokenSerializer>(); + mockSerializer.Setup(o => o.Deserialize("valid-value")).Returns((object)expectedToken); + + AntiForgeryTokenStore tokenStore = new AntiForgeryTokenStore( + config: config, + serializer: mockSerializer.Object); + + // Act + AntiForgeryToken retVal = tokenStore.GetCookieToken(mockHttpContext.Object); + + // Assert + Assert.Same(expectedToken, retVal); + } + + [Fact] + public void GetFormToken_FormFieldIsEmpty_ReturnsNull() + { + // Arrange + Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(); + mockHttpContext.Setup(o => o.Request.Form.Get("form-field-name")).Returns(""); + + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + FormFieldName = "form-field-name" + }; + + AntiForgeryTokenStore tokenStore = new AntiForgeryTokenStore( + config: config, + serializer: null); + + // Act + AntiForgeryToken token = tokenStore.GetFormToken(mockHttpContext.Object); + + // Assert + Assert.Null(token); + } + + [Fact] + public void GetFormToken_FormFieldIsInvalid_PropagatesException() + { + // Arrange + Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(); + mockHttpContext.Setup(o => o.Request.Form.Get("form-field-name")).Returns("invalid-value"); + + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + FormFieldName = "form-field-name" + }; + + HttpAntiForgeryException expectedException = new HttpAntiForgeryException("some exception"); + Mock<MockableAntiForgeryTokenSerializer> mockSerializer = new Mock<MockableAntiForgeryTokenSerializer>(); + mockSerializer.Setup(o => o.Deserialize("invalid-value")).Throws(expectedException); + + AntiForgeryTokenStore tokenStore = new AntiForgeryTokenStore( + config: config, + serializer: mockSerializer.Object); + + // Act & assert + var ex = Assert.Throws<HttpAntiForgeryException>(() => tokenStore.GetFormToken(mockHttpContext.Object)); + Assert.Same(expectedException, ex); + } + + [Fact] + public void GetFormToken_FormFieldIsValid_ReturnsToken() + { + // Arrange + AntiForgeryToken expectedToken = new AntiForgeryToken(); + + Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(); + mockHttpContext.Setup(o => o.Request.Form.Get("form-field-name")).Returns("valid-value"); + + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + FormFieldName = "form-field-name" + }; + + Mock<MockableAntiForgeryTokenSerializer> mockSerializer = new Mock<MockableAntiForgeryTokenSerializer>(); + mockSerializer.Setup(o => o.Deserialize("valid-value")).Returns((object)expectedToken); + + AntiForgeryTokenStore tokenStore = new AntiForgeryTokenStore( + config: config, + serializer: mockSerializer.Object); + + // Act + AntiForgeryToken retVal = tokenStore.GetFormToken(mockHttpContext.Object); + + // Assert + Assert.Same(expectedToken, retVal); + } + + [Theory] + [InlineData(true, true)] + [InlineData(false, null)] + public void SaveCookieToken(bool requireSsl, bool? expectedCookieSecureFlag) + { + // Arrange + AntiForgeryToken token = new AntiForgeryToken(); + HttpCookieCollection cookies = new HttpCookieCollection(); + bool defaultCookieSecureValue = expectedCookieSecureFlag ?? new HttpCookie("name", "value").Secure; // pulled from config; set by ctor + + Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(); + mockHttpContext.Setup(o => o.Response.Cookies).Returns(cookies); + + Mock<MockableAntiForgeryTokenSerializer> mockSerializer = new Mock<MockableAntiForgeryTokenSerializer>(); + mockSerializer.Setup(o => o.Serialize(token)).Returns("serialized-value"); + + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + CookieName = "cookie-name", + RequireSSL = requireSsl + }; + + AntiForgeryTokenStore tokenStore = new AntiForgeryTokenStore( + config: config, + serializer: mockSerializer.Object); + + // Act + tokenStore.SaveCookieToken(mockHttpContext.Object, token); + + // Assert + Assert.Equal(1, cookies.Count); + HttpCookie cookie = cookies["cookie-name"]; + + Assert.NotNull(cookie); + Assert.Equal("serialized-value", cookie.Value); + Assert.True(cookie.HttpOnly); + Assert.Equal(defaultCookieSecureValue, cookie.Secure); + } + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/AntiXsrf/AntiForgeryTokenTest.cs b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/AntiForgeryTokenTest.cs new file mode 100644 index 00000000..8446c5ff --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/AntiForgeryTokenTest.cs @@ -0,0 +1,106 @@ +using Xunit; +using Assert = Microsoft.TestCommon.AssertEx; + +namespace System.Web.Helpers.AntiXsrf.Test +{ + public class AntiForgeryTokenTest + { + [Fact] + public void AdditionalDataProperty() + { + // Arrange + AntiForgeryToken token = new AntiForgeryToken(); + + // Act & assert - 1 + Assert.Equal("", token.AdditionalData); + + // Act & assert - 2 + token.AdditionalData = "additional data"; + Assert.Equal("additional data", token.AdditionalData); + + // Act & assert - 3 + token.AdditionalData = null; + Assert.Equal("", token.AdditionalData); + } + + [Fact] + public void ClaimUidProperty() + { + // Arrange + AntiForgeryToken token = new AntiForgeryToken(); + + // Act & assert - 1 + Assert.Null(token.ClaimUid); + + // Act & assert - 2 + BinaryBlob blob = new BinaryBlob(32); + token.ClaimUid = blob; + Assert.Equal(blob, token.ClaimUid); + + // Act & assert - 3 + token.ClaimUid = null; + Assert.Null(token.ClaimUid); + } + + [Fact] + public void IsSessionTokenProperty() + { + // Arrange + AntiForgeryToken token = new AntiForgeryToken(); + + // Act & assert - 1 + Assert.False(token.IsSessionToken); + + // Act & assert - 2 + token.IsSessionToken = true; + Assert.True(token.IsSessionToken); + + // Act & assert - 3 + token.IsSessionToken = false; + Assert.False(token.IsSessionToken); + } + + [Fact] + public void SecurityTokenProperty() + { + // Arrange + AntiForgeryToken token = new AntiForgeryToken(); + + // Act & assert - 1 + BinaryBlob securityToken = token.SecurityToken; + Assert.NotNull(securityToken); + Assert.Equal(AntiForgeryToken.SecurityTokenBitLength, securityToken.BitLength); + Assert.Equal(securityToken, token.SecurityToken); // check that we're not making a new one each property call + + // Act & assert - 2 + securityToken = new BinaryBlob(64); + token.SecurityToken = securityToken; + Assert.Equal(securityToken, token.SecurityToken); + + // Act & assert - 3 + token.SecurityToken = null; + securityToken = token.SecurityToken; + Assert.NotNull(securityToken); + Assert.Equal(AntiForgeryToken.SecurityTokenBitLength, securityToken.BitLength); + Assert.Equal(securityToken, token.SecurityToken); // check that we're not making a new one each property call + } + + [Fact] + public void UsernameProperty() + { + // Arrange + AntiForgeryToken token = new AntiForgeryToken(); + + // Act & assert - 1 + Assert.Equal("", token.Username); + + // Act & assert - 2 + token.Username = "my username"; + Assert.Equal("my username", token.Username); + + // Act & assert - 3 + token.Username = null; + Assert.Equal("", token.Username); + } + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/AntiXsrf/AntiForgeryWorkerTest.cs b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/AntiForgeryWorkerTest.cs new file mode 100644 index 00000000..f7126c2c --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/AntiForgeryWorkerTest.cs @@ -0,0 +1,401 @@ +using System.Security.Principal; +using System.Web.Helpers.Test; +using System.Web.Mvc; +using Moq; +using Xunit; +using Assert = Microsoft.TestCommon.AssertEx; + +namespace System.Web.Helpers.AntiXsrf.Test +{ + public class AntiForgeryWorkerTest + { + [Fact] + public void ChecksSSL() + { + // Arrange + Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(); + mockHttpContext.Setup(o => o.Request.IsSecureConnection).Returns(false); + + IAntiForgeryConfig config = new MockAntiForgeryConfig() + { + RequireSSL = true + }; + + AntiForgeryWorker worker = new AntiForgeryWorker( + config: config, + serializer: null, + tokenStore: null, + validator: null); + + // Act & assert + var ex = Assert.Throws<InvalidOperationException>(() => worker.Validate(mockHttpContext.Object, "session-token", "field-token")); + Assert.Equal(@"The anti-forgery system has the configuration value AntiForgeryConfig.RequireSsl = true, but the current request is not an SSL request.", ex.Message); + + ex = Assert.Throws<InvalidOperationException>(() => worker.Validate(mockHttpContext.Object)); + Assert.Equal(@"The anti-forgery system has the configuration value AntiForgeryConfig.RequireSsl = true, but the current request is not an SSL request.", ex.Message); + + ex = Assert.Throws<InvalidOperationException>(() => worker.GetFormInputElement(mockHttpContext.Object)); + Assert.Equal(@"The anti-forgery system has the configuration value AntiForgeryConfig.RequireSsl = true, but the current request is not an SSL request.", ex.Message); + + ex = Assert.Throws<InvalidOperationException>(() => { string dummy1, dummy2; worker.GetTokens(mockHttpContext.Object, "cookie-token", out dummy1, out dummy2); }); + Assert.Equal(@"The anti-forgery system has the configuration value AntiForgeryConfig.RequireSsl = true, but the current request is not an SSL request.", ex.Message); + } + + [Fact] + public void GetFormInputElement_ExistingInvalidCookieToken() + { + // Arrange + GenericIdentity identity = new GenericIdentity("some-user"); + Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(); + mockHttpContext.Setup(o => o.User).Returns(new GenericPrincipal(identity, new string[0])); + + AntiForgeryToken oldCookieToken = new AntiForgeryToken() { IsSessionToken = true }; + AntiForgeryToken newCookieToken = new AntiForgeryToken() { IsSessionToken = true }; + AntiForgeryToken formToken = new AntiForgeryToken(); + + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + FormFieldName = "form-field-name" + }; + + Mock<MockableAntiForgeryTokenSerializer> mockSerializer = new Mock<MockableAntiForgeryTokenSerializer>(MockBehavior.Strict); + mockSerializer.Setup(o => o.Serialize(formToken)).Returns("serialized-form-token"); + + Mock<MockableTokenStore> mockTokenStore = new Mock<MockableTokenStore>(MockBehavior.Strict); + mockTokenStore.Setup(o => o.GetCookieToken(mockHttpContext.Object)).Returns(oldCookieToken); + mockTokenStore.Setup(o => o.SaveCookieToken(mockHttpContext.Object, newCookieToken)).Verifiable(); + + Mock<MockableTokenValidator> mockValidator = new Mock<MockableTokenValidator>(MockBehavior.Strict); + mockValidator.Setup(o => o.GenerateFormToken(mockHttpContext.Object, identity, newCookieToken)).Returns(formToken); + mockValidator.Setup(o => o.IsCookieTokenValid(oldCookieToken)).Returns(false); + mockValidator.Setup(o => o.IsCookieTokenValid(newCookieToken)).Returns(true); + mockValidator.Setup(o => o.GenerateCookieToken()).Returns(newCookieToken); + + AntiForgeryWorker worker = new AntiForgeryWorker( + config: config, + serializer: mockSerializer.Object, + tokenStore: mockTokenStore.Object, + validator: mockValidator.Object); + + // Act + TagBuilder retVal = worker.GetFormInputElement(mockHttpContext.Object); + + // Assert + Assert.Equal(@"<input name=""form-field-name"" type=""hidden"" value=""serialized-form-token"" />", retVal.ToString(TagRenderMode.SelfClosing)); + mockTokenStore.Verify(); + } + + [Fact] + public void GetFormInputElement_ExistingInvalidCookieToken_SwallowsExceptions() + { + // Arrange + GenericIdentity identity = new GenericIdentity("some-user"); + Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(); + mockHttpContext.Setup(o => o.User).Returns(new GenericPrincipal(identity, new string[0])); + + AntiForgeryToken oldCookieToken = new AntiForgeryToken() { IsSessionToken = true }; + AntiForgeryToken newCookieToken = new AntiForgeryToken() { IsSessionToken = true }; + AntiForgeryToken formToken = new AntiForgeryToken(); + + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + FormFieldName = "form-field-name" + }; + + Mock<MockableAntiForgeryTokenSerializer> mockSerializer = new Mock<MockableAntiForgeryTokenSerializer>(MockBehavior.Strict); + mockSerializer.Setup(o => o.Serialize(formToken)).Returns("serialized-form-token"); + + Mock<MockableTokenStore> mockTokenStore = new Mock<MockableTokenStore>(MockBehavior.Strict); + mockTokenStore.Setup(o => o.GetCookieToken(mockHttpContext.Object)).Throws(new Exception("should be swallowed")); + mockTokenStore.Setup(o => o.SaveCookieToken(mockHttpContext.Object, newCookieToken)).Verifiable(); + + Mock<MockableTokenValidator> mockValidator = new Mock<MockableTokenValidator>(MockBehavior.Strict); + mockValidator.Setup(o => o.GenerateFormToken(mockHttpContext.Object, identity, newCookieToken)).Returns(formToken); + mockValidator.Setup(o => o.IsCookieTokenValid(null)).Returns(false); + mockValidator.Setup(o => o.IsCookieTokenValid(newCookieToken)).Returns(true); + mockValidator.Setup(o => o.GenerateCookieToken()).Returns(newCookieToken); + + AntiForgeryWorker worker = new AntiForgeryWorker( + config: config, + serializer: mockSerializer.Object, + tokenStore: mockTokenStore.Object, + validator: mockValidator.Object); + + // Act + TagBuilder retVal = worker.GetFormInputElement(mockHttpContext.Object); + + // Assert + Assert.Equal(@"<input name=""form-field-name"" type=""hidden"" value=""serialized-form-token"" />", retVal.ToString(TagRenderMode.SelfClosing)); + mockTokenStore.Verify(); + } + + [Fact] + public void GetFormInputElement_ExistingValidCookieToken() + { + // Arrange + GenericIdentity identity = new GenericIdentity("some-user"); + Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(); + mockHttpContext.Setup(o => o.User).Returns(new GenericPrincipal(identity, new string[0])); + + AntiForgeryToken cookieToken = new AntiForgeryToken() { IsSessionToken = true }; + AntiForgeryToken formToken = new AntiForgeryToken(); + + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + FormFieldName = "form-field-name" + }; + + Mock<MockableAntiForgeryTokenSerializer> mockSerializer = new Mock<MockableAntiForgeryTokenSerializer>(MockBehavior.Strict); + mockSerializer.Setup(o => o.Serialize(formToken)).Returns("serialized-form-token"); + + Mock<MockableTokenStore> mockTokenStore = new Mock<MockableTokenStore>(MockBehavior.Strict); + mockTokenStore.Setup(o => o.GetCookieToken(mockHttpContext.Object)).Returns(cookieToken); + + Mock<MockableTokenValidator> mockValidator = new Mock<MockableTokenValidator>(MockBehavior.Strict); + mockValidator.Setup(o => o.GenerateFormToken(mockHttpContext.Object, identity, cookieToken)).Returns(formToken); + mockValidator.Setup(o => o.IsCookieTokenValid(cookieToken)).Returns(true); + + AntiForgeryWorker worker = new AntiForgeryWorker( + config: config, + serializer: mockSerializer.Object, + tokenStore: mockTokenStore.Object, + validator: mockValidator.Object); + + // Act + TagBuilder retVal = worker.GetFormInputElement(mockHttpContext.Object); + + // Assert + Assert.Equal(@"<input name=""form-field-name"" type=""hidden"" value=""serialized-form-token"" />", retVal.ToString(TagRenderMode.SelfClosing)); + } + + [Fact] + public void GetTokens_ExistingInvalidCookieToken() + { + // Arrange + GenericIdentity identity = new GenericIdentity("some-user"); + Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(); + mockHttpContext.Setup(o => o.User).Returns(new GenericPrincipal(identity, new string[0])); + + AntiForgeryToken oldCookieToken = new AntiForgeryToken() { IsSessionToken = true }; + AntiForgeryToken newCookieToken = new AntiForgeryToken() { IsSessionToken = true }; + AntiForgeryToken formToken = new AntiForgeryToken(); + + Mock<MockableAntiForgeryTokenSerializer> mockSerializer = new Mock<MockableAntiForgeryTokenSerializer>(MockBehavior.Strict); + mockSerializer.Setup(o => o.Deserialize("serialized-old-cookie-token")).Returns(oldCookieToken); + mockSerializer.Setup(o => o.Serialize(newCookieToken)).Returns("serialized-new-cookie-token"); + mockSerializer.Setup(o => o.Serialize(formToken)).Returns("serialized-form-token"); + + Mock<MockableTokenValidator> mockValidator = new Mock<MockableTokenValidator>(MockBehavior.Strict); + mockValidator.Setup(o => o.GenerateFormToken(mockHttpContext.Object, identity, newCookieToken)).Returns(formToken); + mockValidator.Setup(o => o.IsCookieTokenValid(oldCookieToken)).Returns(false); + mockValidator.Setup(o => o.IsCookieTokenValid(newCookieToken)).Returns(true); + mockValidator.Setup(o => o.GenerateCookieToken()).Returns(newCookieToken); + + AntiForgeryWorker worker = new AntiForgeryWorker( + config: new MockAntiForgeryConfig(), + serializer: mockSerializer.Object, + tokenStore: null, + validator: mockValidator.Object); + + // Act + string serializedNewCookieToken, serializedFormToken; + worker.GetTokens(mockHttpContext.Object, "serialized-old-cookie-token", out serializedNewCookieToken, out serializedFormToken); + + // Assert + Assert.Equal("serialized-new-cookie-token", serializedNewCookieToken); + Assert.Equal("serialized-form-token", serializedFormToken); + } + + [Fact] + public void GetTokens_ExistingInvalidCookieToken_SwallowsExceptions() + { + // Arrange + GenericIdentity identity = new GenericIdentity("some-user"); + Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(); + mockHttpContext.Setup(o => o.User).Returns(new GenericPrincipal(identity, new string[0])); + + AntiForgeryToken oldCookieToken = new AntiForgeryToken() { IsSessionToken = true }; + AntiForgeryToken newCookieToken = new AntiForgeryToken() { IsSessionToken = true }; + AntiForgeryToken formToken = new AntiForgeryToken(); + + Mock<MockableAntiForgeryTokenSerializer> mockSerializer = new Mock<MockableAntiForgeryTokenSerializer>(MockBehavior.Strict); + mockSerializer.Setup(o => o.Deserialize("serialized-old-cookie-token")).Throws(new Exception("should be swallowed")); + mockSerializer.Setup(o => o.Serialize(newCookieToken)).Returns("serialized-new-cookie-token"); + mockSerializer.Setup(o => o.Serialize(formToken)).Returns("serialized-form-token"); + + Mock<MockableTokenValidator> mockValidator = new Mock<MockableTokenValidator>(MockBehavior.Strict); + mockValidator.Setup(o => o.GenerateFormToken(mockHttpContext.Object, identity, newCookieToken)).Returns(formToken); + mockValidator.Setup(o => o.IsCookieTokenValid(null)).Returns(false); + mockValidator.Setup(o => o.IsCookieTokenValid(newCookieToken)).Returns(true); + mockValidator.Setup(o => o.GenerateCookieToken()).Returns(newCookieToken); + + AntiForgeryWorker worker = new AntiForgeryWorker( + config: new MockAntiForgeryConfig(), + serializer: mockSerializer.Object, + tokenStore: null, + validator: mockValidator.Object); + + // Act + string serializedNewCookieToken, serializedFormToken; + worker.GetTokens(mockHttpContext.Object, "serialized-old-cookie-token", out serializedNewCookieToken, out serializedFormToken); + + // Assert + Assert.Equal("serialized-new-cookie-token", serializedNewCookieToken); + Assert.Equal("serialized-form-token", serializedFormToken); + } + + [Fact] + public void GetTokens_ExistingValidCookieToken() + { + // Arrange + GenericIdentity identity = new GenericIdentity("some-user"); + Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(); + mockHttpContext.Setup(o => o.User).Returns(new GenericPrincipal(identity, new string[0])); + + AntiForgeryToken cookieToken = new AntiForgeryToken() { IsSessionToken = true }; + AntiForgeryToken formToken = new AntiForgeryToken(); + + Mock<MockableAntiForgeryTokenSerializer> mockSerializer = new Mock<MockableAntiForgeryTokenSerializer>(MockBehavior.Strict); + mockSerializer.Setup(o => o.Deserialize("serialized-old-cookie-token")).Returns(cookieToken); + mockSerializer.Setup(o => o.Serialize(formToken)).Returns("serialized-form-token"); + + Mock<MockableTokenValidator> mockValidator = new Mock<MockableTokenValidator>(MockBehavior.Strict); + mockValidator.Setup(o => o.GenerateFormToken(mockHttpContext.Object, identity, cookieToken)).Returns(formToken); + mockValidator.Setup(o => o.IsCookieTokenValid(cookieToken)).Returns(true); + + AntiForgeryWorker worker = new AntiForgeryWorker( + config: new MockAntiForgeryConfig(), + serializer: mockSerializer.Object, + tokenStore: null, + validator: mockValidator.Object); + + // Act + string serializedNewCookieToken, serializedFormToken; + worker.GetTokens(mockHttpContext.Object, "serialized-old-cookie-token", out serializedNewCookieToken, out serializedFormToken); + + // Assert + Assert.Null(serializedNewCookieToken); + Assert.Equal("serialized-form-token", serializedFormToken); + } + + [Fact] + public void Validate_FromStrings_Failure() + { + // Arrange + GenericIdentity identity = new GenericIdentity("some-user"); + Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(); + mockHttpContext.Setup(o => o.User).Returns(new GenericPrincipal(identity, new string[0])); + + AntiForgeryToken cookieToken = new AntiForgeryToken(); + AntiForgeryToken formToken = new AntiForgeryToken(); + + Mock<MockableAntiForgeryTokenSerializer> mockSerializer = new Mock<MockableAntiForgeryTokenSerializer>(); + mockSerializer.Setup(o => o.Deserialize("cookie-token")).Returns(cookieToken); + mockSerializer.Setup(o => o.Deserialize("form-token")).Returns(formToken); + + Mock<MockableTokenValidator> mockValidator = new Mock<MockableTokenValidator>(); + mockValidator.Setup(o => o.ValidateTokens(mockHttpContext.Object, identity, cookieToken, formToken)).Throws(new HttpAntiForgeryException("my-message")); + + AntiForgeryWorker worker = new AntiForgeryWorker( + config: new MockAntiForgeryConfig(), + serializer: mockSerializer.Object, + tokenStore: null, + validator: mockValidator.Object); + + // Act & assert + var ex = Assert.Throws<HttpAntiForgeryException>(() => worker.Validate(mockHttpContext.Object, "cookie-token", "form-token")); + Assert.Equal("my-message", ex.Message); + } + + [Fact] + public void Validate_FromStrings_Success() + { + // Arrange + GenericIdentity identity = new GenericIdentity("some-user"); + Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(); + mockHttpContext.Setup(o => o.User).Returns(new GenericPrincipal(identity, new string[0])); + + AntiForgeryToken cookieToken = new AntiForgeryToken(); + AntiForgeryToken formToken = new AntiForgeryToken(); + + Mock<MockableAntiForgeryTokenSerializer> mockSerializer = new Mock<MockableAntiForgeryTokenSerializer>(); + mockSerializer.Setup(o => o.Deserialize("cookie-token")).Returns(cookieToken); + mockSerializer.Setup(o => o.Deserialize("form-token")).Returns(formToken); + + Mock<MockableTokenValidator> mockValidator = new Mock<MockableTokenValidator>(); + mockValidator.Setup(o => o.ValidateTokens(mockHttpContext.Object, identity, cookieToken, formToken)).Verifiable(); + + AntiForgeryWorker worker = new AntiForgeryWorker( + config: new MockAntiForgeryConfig(), + serializer: mockSerializer.Object, + tokenStore: null, + validator: mockValidator.Object); + + // Act + worker.Validate(mockHttpContext.Object, "cookie-token", "form-token"); + + // Assert + mockValidator.Verify(); + } + + [Fact] + public void Validate_FromStore_Failure() + { + // Arrange + GenericIdentity identity = new GenericIdentity("some-user"); + Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(); + mockHttpContext.Setup(o => o.User).Returns(new GenericPrincipal(identity, new string[0])); + + AntiForgeryToken cookieToken = new AntiForgeryToken(); + AntiForgeryToken formToken = new AntiForgeryToken(); + + Mock<MockableTokenStore> mockTokenStore = new Mock<MockableTokenStore>(); + mockTokenStore.Setup(o => o.GetCookieToken(mockHttpContext.Object)).Returns(cookieToken); + mockTokenStore.Setup(o => o.GetFormToken(mockHttpContext.Object)).Returns(formToken); + + Mock<MockableTokenValidator> mockValidator = new Mock<MockableTokenValidator>(); + mockValidator.Setup(o => o.ValidateTokens(mockHttpContext.Object, identity, cookieToken, formToken)).Throws(new HttpAntiForgeryException("my-message")); + + AntiForgeryWorker worker = new AntiForgeryWorker( + config: new MockAntiForgeryConfig(), + serializer: null, + tokenStore: mockTokenStore.Object, + validator: mockValidator.Object); + + // Act & assert + var ex = Assert.Throws<HttpAntiForgeryException>(() => worker.Validate(mockHttpContext.Object)); + Assert.Equal("my-message", ex.Message); + } + + [Fact] + public void Validate_FromStore_Success() + { + // Arrange + GenericIdentity identity = new GenericIdentity("some-user"); + Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>(); + mockHttpContext.Setup(o => o.User).Returns(new GenericPrincipal(identity, new string[0])); + + AntiForgeryToken cookieToken = new AntiForgeryToken(); + AntiForgeryToken formToken = new AntiForgeryToken(); + + Mock<MockableTokenStore> mockTokenStore = new Mock<MockableTokenStore>(); + mockTokenStore.Setup(o => o.GetCookieToken(mockHttpContext.Object)).Returns(cookieToken); + mockTokenStore.Setup(o => o.GetFormToken(mockHttpContext.Object)).Returns(formToken); + + Mock<MockableTokenValidator> mockValidator = new Mock<MockableTokenValidator>(); + mockValidator.Setup(o => o.ValidateTokens(mockHttpContext.Object, identity, cookieToken, formToken)).Verifiable(); + + AntiForgeryWorker worker = new AntiForgeryWorker( + config: new MockAntiForgeryConfig(), + serializer: null, + tokenStore: mockTokenStore.Object, + validator: mockValidator.Object); + + // Act + worker.Validate(mockHttpContext.Object); + + // Assert + mockValidator.Verify(); + } + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/AntiXsrf/BinaryBlobTest.cs b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/BinaryBlobTest.cs new file mode 100644 index 00000000..23e509b3 --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/BinaryBlobTest.cs @@ -0,0 +1,127 @@ +using Xunit; +using Xunit.Extensions; +using Assert = Microsoft.TestCommon.AssertEx; + +namespace System.Web.Helpers.AntiXsrf.Test +{ + public class BinaryBlobTest + { + [Fact] + public void Ctor_BitLength() + { + // Act + BinaryBlob blob = new BinaryBlob(bitLength: 64); + byte[] data = blob.GetData(); + + // Assert + Assert.Equal(64, blob.BitLength); + Assert.Equal(64 / 8, data.Length); + Assert.NotEqual(new byte[64 / 8], data); // should not be a zero-filled array + } + + [Theory] + [InlineData(24)] + [InlineData(33)] + public void Ctor_BitLength_Bad(int bitLength) + { + // Act & assert + var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new BinaryBlob(bitLength)); + Assert.Equal("bitLength", ex.ParamName); + } + + [Fact] + public void Ctor_BitLength_ProducesDifferentValues() + { + // Act + BinaryBlob blobA = new BinaryBlob(bitLength: 64); + BinaryBlob blobB = new BinaryBlob(bitLength: 64); + + // Assert + Assert.NotEqual(blobA.GetData(), blobB.GetData()); + } + + [Fact] + public void Ctor_Data() + { + // Arrange + byte[] expectedData = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + + // Act + BinaryBlob blob = new BinaryBlob(32, expectedData); + + // Assert + Assert.Equal(32, blob.BitLength); + Assert.Equal(expectedData, blob.GetData()); + } + + [Theory] + [InlineData((object[])null)] + [InlineData(new byte[] { 0x01, 0x02, 0x03 })] + public void Ctor_Data_Bad(byte[] data) + { + // Act & assert + var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new BinaryBlob(32, data)); + Assert.Equal("data", ex.ParamName); + } + + [Fact] + public void Equals_DifferentData_ReturnsFalse() + { + // Arrange + object blobA = new BinaryBlob(32, new byte[] { 0x01, 0x02, 0x03, 0x04 }); + object blobB = new BinaryBlob(32, new byte[] { 0x04, 0x03, 0x02, 0x01 }); + + // Act & assert + Assert.NotEqual(blobA, blobB); + } + + [Fact] + public void Equals_NotABlob_ReturnsFalse() + { + // Arrange + object blobA = new BinaryBlob(32); + object blobB = "hello"; + + // Act & assert + Assert.NotEqual(blobA, blobB); + } + + [Fact] + public void Equals_Null_ReturnsFalse() + { + // Arrange + object blobA = new BinaryBlob(32); + object blobB = null; + + // Act & assert + Assert.NotEqual(blobA, blobB); + } + + [Fact] + public void Equals_SameData_ReturnsTrue() + { + // Arrange + object blobA = new BinaryBlob(32, new byte[] { 0x01, 0x02, 0x03, 0x04 }); + object blobB = new BinaryBlob(32, new byte[] { 0x01, 0x02, 0x03, 0x04 }); + + // Act & assert + Assert.Equal(blobA, blobB); + } + + [Fact] + public void GetHashCodeTest() + { + // Arrange + byte[] blobData = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + int expectedHashCode = BitConverter.ToInt32(blobData, 0); + + BinaryBlob blob = new BinaryBlob(32, blobData); + + // Act + int actualHashCode = blob.GetHashCode(); + + // Assert + Assert.Equal(expectedHashCode, actualHashCode); + } + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/AntiXsrf/ClaimUidExtractorTest.cs b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/ClaimUidExtractorTest.cs new file mode 100644 index 00000000..f5262588 --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/ClaimUidExtractorTest.cs @@ -0,0 +1,237 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Security.Principal; +using System.Web.Helpers.Claims; +using System.Web.Helpers.Claims.Test; +using System.Web.Helpers.Test; +using Moq; +using Xunit; +using Xunit.Extensions; +using Assert = Microsoft.TestCommon.AssertEx; + +namespace System.Web.Helpers.AntiXsrf.Test +{ + public class ClaimUidExtractorTest + { + [Fact] + public void ExtractClaimUid_NullIdentity() + { + // Arrange + ClaimUidExtractor extractor = new ClaimUidExtractor( + config: null, + claimsIdentityConverter: null); + + // Act + BinaryBlob retVal = extractor.ExtractClaimUid(null); + + // Assert + Assert.Null(retVal); + } + + [Fact] + public void ExtractClaimUid_Unauthenticated() + { + // Arrange + ClaimUidExtractor extractor = new ClaimUidExtractor( + config: null, + claimsIdentityConverter: null); + + Mock<IIdentity> mockIdentity = new Mock<IIdentity>(); + mockIdentity.Setup(o => o.IsAuthenticated).Returns(false); + + // Act + BinaryBlob retVal = extractor.ExtractClaimUid(mockIdentity.Object); + + // Assert + Assert.Null(retVal); + } + + [Fact] + public void ExtractClaimUid_ClaimsIdentityHeuristicsSuppressed() + { + // Arrange + GenericIdentity identity = new GenericIdentity("the-user"); + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + SuppressIdentityHeuristicChecks = true + }; + + ClaimUidExtractor extractor = new ClaimUidExtractor( + config: config, + claimsIdentityConverter: null); + + // Act + BinaryBlob retVal = extractor.ExtractClaimUid(identity); + + // Assert + Assert.Null(retVal); + } + + [Fact] + public void ExtractClaimUid_NotAClaimsIdentity() + { + // Arrange + Mock<IIdentity> mockIdentity = new Mock<IIdentity>(); + mockIdentity.Setup(o => o.IsAuthenticated).Returns(true); + MockAntiForgeryConfig config = new MockAntiForgeryConfig(); + ClaimsIdentityConverter converter = new ClaimsIdentityConverter(new Func<IIdentity, ClaimsIdentity>[0]); + + ClaimUidExtractor extractor = new ClaimUidExtractor( + config: config, + claimsIdentityConverter: converter); + + // Act + BinaryBlob retVal = extractor.ExtractClaimUid(mockIdentity.Object); + + // Assert + Assert.Null(retVal); + } + + [Fact] + public void ExtractClaimUid_ClaimsIdentity() + { + // Arrange + Mock<IIdentity> mockIdentity = new Mock<IIdentity>(); + mockIdentity.Setup(o => o.IsAuthenticated).Returns(true); + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + UniqueClaimTypeIdentifier = "unique-identifier" + }; + ClaimsIdentityConverter converter = new ClaimsIdentityConverter(new Func<IIdentity, ClaimsIdentity>[] { + identity => + { + Assert.Equal(mockIdentity.Object, identity); + MockClaimsIdentity claimsIdentity = new MockClaimsIdentity(); + claimsIdentity.AddClaim("unique-identifier", "some-value"); + return claimsIdentity; + } + }); + + ClaimUidExtractor extractor = new ClaimUidExtractor( + config: config, + claimsIdentityConverter: converter); + + // Act + BinaryBlob retVal = extractor.ExtractClaimUid(mockIdentity.Object); + + // Assert + Assert.NotNull(retVal); + Assert.Equal("CA9CCFF86F903FBB7505BAAA9F222E49EC2A1E8FAD630AE73DE180BD679751ED", HexUtil.HexEncode(retVal.GetData())); + } + + [Theory] + [DefaultUniqueClaimTypes_NotPresent_Data] + public void DefaultUniqueClaimTypes_NotPresent_Throws(object identity) + { + // Arrange + ClaimsIdentity claimsIdentity = (ClaimsIdentity)identity; + + // Act & assert + var ex = Assert.Throws<InvalidOperationException>(() => ClaimUidExtractor.GetUniqueIdentifierParameters(claimsIdentity, null)); + Assert.Equal(@"A claim of type 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier' or 'http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider' was not present on the provided ClaimsIdentity. To enable anti-forgery token support with claims-based authentication, please verify that the configured claims provider is providing both of these claims on the ClaimsIdentity instances it generates. If the configured claims provider instead uses a different claim type as a unique identifier, it can be configured by setting the static property AntiForgeryConfig.UniqueClaimTypeIdentifier.", ex.Message); + } + + [Fact] + public void DefaultUniqueClaimTypes_Present() + { + // Arrange + MockClaimsIdentity identity = new MockClaimsIdentity(); + identity.AddClaim("fooClaim", "fooClaimValue"); + identity.AddClaim(ClaimUidExtractor.NameIdentifierClaimType, "nameIdentifierValue"); + identity.AddClaim(ClaimUidExtractor.IdentityProviderClaimType, "identityProviderValue"); + + // Act + var retVal = ClaimUidExtractor.GetUniqueIdentifierParameters(identity, null); + + // Assert + Assert.Equal(new string[] { + ClaimUidExtractor.NameIdentifierClaimType, + "nameIdentifierValue", + ClaimUidExtractor.IdentityProviderClaimType, + "identityProviderValue" + }, retVal); + } + + [Fact] + public void ExplicitUniqueClaimType_Present() + { + // Arrange + MockClaimsIdentity identity = new MockClaimsIdentity(); + identity.AddClaim("fooClaim", "fooClaimValue"); + identity.AddClaim(ClaimUidExtractor.NameIdentifierClaimType, "nameIdentifierValue"); + identity.AddClaim(ClaimUidExtractor.IdentityProviderClaimType, "identityProviderValue"); + + // Act + var retVal = ClaimUidExtractor.GetUniqueIdentifierParameters(identity, "fooClaim"); + + // Assert + Assert.Equal(new string[] { + "fooClaim", + "fooClaimValue" + }, retVal); + } + + [Theory] + [ExplicitUniqueClaimType_NotPresent_Data] + public void ExplicitUniqueClaimType_NotPresent_Throws(object identity) + { + // Arrange + ClaimsIdentity claimsIdentity = (ClaimsIdentity)identity; + + // Act & assert + var ex = Assert.Throws<InvalidOperationException>(() => ClaimUidExtractor.GetUniqueIdentifierParameters(claimsIdentity, "fooClaim")); + Assert.Equal(@"A claim of type 'fooClaim' was not present on the provided ClaimsIdentity.", ex.Message); + } + + private sealed class DefaultUniqueClaimTypes_NotPresent_DataAttribute : DataAttribute + { + public override IEnumerable<object[]> GetData(MethodInfo methodUnderTest, Type[] parameterTypes) + { + MockClaimsIdentity identity1 = new MockClaimsIdentity(); + identity1.AddClaim(ClaimUidExtractor.IdentityProviderClaimType, "identityProviderValue"); + yield return new object[] { identity1 }; + + MockClaimsIdentity identity2 = new MockClaimsIdentity(); + identity2.AddClaim(ClaimUidExtractor.NameIdentifierClaimType, String.Empty); + identity2.AddClaim(ClaimUidExtractor.IdentityProviderClaimType, "identityProviderValue"); + yield return new object[] { identity2 }; + + MockClaimsIdentity identity3 = new MockClaimsIdentity(); + identity3.AddClaim(ClaimUidExtractor.NameIdentifierClaimType, "nameIdentifierValue"); + yield return new object[] { identity3 }; + + MockClaimsIdentity identity4 = new MockClaimsIdentity(); + identity4.AddClaim(ClaimUidExtractor.NameIdentifierClaimType, "nameIdentifierValue"); + identity4.AddClaim(ClaimUidExtractor.IdentityProviderClaimType, String.Empty); + yield return new object[] { identity4 }; + + MockClaimsIdentity identity5 = new MockClaimsIdentity(); + identity5.AddClaim(ClaimUidExtractor.NameIdentifierClaimType.ToUpper(), "nameIdentifierValue"); + identity5.AddClaim(ClaimUidExtractor.IdentityProviderClaimType.ToUpper(), "identityProviderValue"); + yield return new object[] { identity5 }; + } + } + + private sealed class ExplicitUniqueClaimType_NotPresent_DataAttribute : DataAttribute + { + public override IEnumerable<object[]> GetData(MethodInfo methodUnderTest, Type[] parameterTypes) + { + MockClaimsIdentity identity1 = new MockClaimsIdentity(); + yield return new object[] { identity1 }; + + MockClaimsIdentity identity2 = new MockClaimsIdentity(); + identity2.AddClaim("fooClaim", String.Empty); + yield return new object[] { identity2 }; + + MockClaimsIdentity identity3 = new MockClaimsIdentity(); + identity3.AddClaim("FOOCLAIM", "fooClaimValue"); + yield return new object[] { identity3 }; + + MockClaimsIdentity identity4 = new MockClaimsIdentity(); + identity4.AddClaim(ClaimUidExtractor.NameIdentifierClaimType, "nameIdentifierValue"); + identity4.AddClaim(ClaimUidExtractor.IdentityProviderClaimType, "identityProviderValue"); + yield return new object[] { identity4 }; + } + } + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/AntiXsrf/HexUtil.cs b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/HexUtil.cs new file mode 100644 index 00000000..52359eb8 --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/HexUtil.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace System.Web.Helpers.AntiXsrf.Test +{ + internal static class HexUtil + { + public static string HexEncode(byte[] data) + { + StringBuilder sb = new StringBuilder(data.Length * 2); + foreach (byte b in data) + { + sb.AppendFormat(CultureInfo.InvariantCulture, "{0:X2}", b); + } + return sb.ToString(); + } + + public static byte[] HexDecode(string input) + { + List<byte> bytes = new List<byte>(input.Length / 2); + for (int i = 0; i < input.Length; i += 2) + { + bytes.Add(Byte.Parse(input.Substring(i, 2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture)); + } + return bytes.ToArray(); + } + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MachineKeyCryptoSystemTest.cs b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MachineKeyCryptoSystemTest.cs new file mode 100644 index 00000000..9869aea8 --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MachineKeyCryptoSystemTest.cs @@ -0,0 +1,120 @@ +using System.Web.Security; +using Xunit; +using Xunit.Extensions; +using Assert = Microsoft.TestCommon.AssertEx; + +namespace System.Web.Helpers.AntiXsrf.Test +{ + public class MachineKeyCryptoSystemTest + { + private static readonly MachineKeyCryptoSystem _dummyCryptoSystem = new MachineKeyCryptoSystem(HexEncoder, HexDecoder); + + [Fact] + public void Base64ToHex() + { + // Arrange + string base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_0"; + string hex = "00108310518720928B30D38F41149351559761969B71D79F8218A39259A7A29AABB2DBAFC31CB3D35DB7E39EBBF3DFBF"; + + // Act + string retVal = MachineKeyCryptoSystem.Base64ToHex(base64); + + // Assert + Assert.Equal(hex, retVal); + } + + [Fact] + public void Base64ToHex_HexToBase64_RoundTrips() + { + for (int i = 0; i <= Byte.MaxValue; i++) + { + // Arrange + string hex = String.Format("{0:X2}", i); + + // Act + string retVal = MachineKeyCryptoSystem.Base64ToHex(MachineKeyCryptoSystem.HexToBase64(hex)); + + // Assert + Assert.Equal(hex, retVal); + } + } + + [Fact] + public void HexToBase64() + { + // Arrange + string base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_0"; + string hex = "00108310518720928B30D38F41149351559761969B71D79F8218A39259A7A29AABB2DBAFC31CB3D35DB7E39EBBF3DFBF"; + + // Act + string retVal = MachineKeyCryptoSystem.HexToBase64(hex); + + // Assert + Assert.Equal(base64, retVal); + } + + [Fact] + public void Protect() + { + // Arrange + byte[] data = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }; + + // Act + string retVal = _dummyCryptoSystem.Protect(data); + + // Assert + Assert.Equal("hYfyZgECAwQF0", retVal); + } + + [Theory] + [InlineData("hYfyZwECAwQF0")] // bad MagicHeader + [InlineData("hYfy0")] // too short to contain MagicHeader + public void Unprotect_Failure(string protectedData) + { + // Act + byte[] retVal = _dummyCryptoSystem.Unprotect(protectedData); + + // Assert + Assert.Null(retVal); + } + + [Fact] + public void Unprotect_Success() + { + // Arrange + byte[] expected = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }; + + // Act + byte[] retVal = _dummyCryptoSystem.Unprotect("hYfyZgECAwQF0"); + + // Assert + Assert.Equal(expected, retVal); + } + + [Fact] + public void Protect_Unprotect_RoundTrips() + { + // Arrange + byte[] data = new byte[1024]; + new Random().NextBytes(data); + + // Act + byte[] roundTripped = _dummyCryptoSystem.Unprotect(_dummyCryptoSystem.Protect(data)); + + // Assert + Assert.Equal(data, roundTripped); + } + + private static string HexEncoder(byte[] data, MachineKeyProtection protection) + { + Assert.Equal(MachineKeyProtection.All, protection); + return HexUtil.HexEncode(data); + } + + private static byte[] HexDecoder(string input, MachineKeyProtection protection) + { + Assert.Equal(MachineKeyProtection.All, protection); + return HexUtil.HexDecode(input); + } + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MockAntiForgeryConfig.cs b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MockAntiForgeryConfig.cs new file mode 100644 index 00000000..4cf2663d --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MockAntiForgeryConfig.cs @@ -0,0 +1,41 @@ +namespace System.Web.Helpers.AntiXsrf.Test +{ + public sealed class MockAntiForgeryConfig : IAntiForgeryConfig + { + public IAntiForgeryAdditionalDataProvider AdditionalDataProvider + { + get; + set; + } + + public string CookieName + { + get; + set; + } + + public string FormFieldName + { + get; + set; + } + + public bool RequireSSL + { + get; + set; + } + + public bool SuppressIdentityHeuristicChecks + { + get; + set; + } + + public string UniqueClaimTypeIdentifier + { + get; + set; + } + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MockableAntiForgeryTokenSerializer.cs b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MockableAntiForgeryTokenSerializer.cs new file mode 100644 index 00000000..4e44d905 --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MockableAntiForgeryTokenSerializer.cs @@ -0,0 +1,19 @@ +namespace System.Web.Helpers.AntiXsrf.Test +{ + // An IAntiForgeryTokenSerializer that can be passed to MoQ. + public abstract class MockableAntiForgeryTokenSerializer : IAntiForgeryTokenSerializer + { + public abstract object Deserialize(string serializedToken); + public abstract string Serialize(object token); + + AntiForgeryToken IAntiForgeryTokenSerializer.Deserialize(string serializedToken) + { + return (AntiForgeryToken)Deserialize(serializedToken); + } + + string IAntiForgeryTokenSerializer.Serialize(AntiForgeryToken token) + { + return Serialize(token); + } + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MockableClaimUidExtractor.cs b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MockableClaimUidExtractor.cs new file mode 100644 index 00000000..47a309ce --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MockableClaimUidExtractor.cs @@ -0,0 +1,15 @@ +using System.Security.Principal; + +namespace System.Web.Helpers.AntiXsrf.Test +{ + // An IClaimUidExtractor that can be passed to MoQ + public abstract class MockableClaimUidExtractor : IClaimUidExtractor + { + public abstract object ExtractClaimUid(IIdentity identity); + + BinaryBlob IClaimUidExtractor.ExtractClaimUid(IIdentity identity) + { + return (BinaryBlob)ExtractClaimUid(identity); + } + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MockableCryptoSystem.cs b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MockableCryptoSystem.cs new file mode 100644 index 00000000..952e371f --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MockableCryptoSystem.cs @@ -0,0 +1,9 @@ +namespace System.Web.Helpers.AntiXsrf.Test +{ + // An ICryptoSystem that can be passed to MoQ + public abstract class MockableCryptoSystem : ICryptoSystem + { + public abstract string Protect(byte[] data); + public abstract byte[] Unprotect(string protectedData); + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MockableTokenStore.cs b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MockableTokenStore.cs new file mode 100644 index 00000000..36efcb18 --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MockableTokenStore.cs @@ -0,0 +1,27 @@ +using System.Security.Principal; + +namespace System.Web.Helpers.AntiXsrf.Test +{ + // An ITokenStore that can be passed to MoQ + public abstract class MockableTokenStore : ITokenStore + { + public abstract object GetCookieToken(HttpContextBase httpContext); + public abstract object GetFormToken(HttpContextBase httpContext); + public abstract void SaveCookieToken(HttpContextBase httpContext, object token); + + AntiForgeryToken ITokenStore.GetCookieToken(HttpContextBase httpContext) + { + return (AntiForgeryToken)GetCookieToken(httpContext); + } + + AntiForgeryToken ITokenStore.GetFormToken(HttpContextBase httpContext) + { + return (AntiForgeryToken)GetFormToken(httpContext); + } + + void ITokenStore.SaveCookieToken(HttpContextBase httpContext, AntiForgeryToken token) + { + SaveCookieToken(httpContext, (AntiForgeryToken)token); + } + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MockableTokenValidator.cs b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MockableTokenValidator.cs new file mode 100644 index 00000000..1f77eef0 --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/MockableTokenValidator.cs @@ -0,0 +1,33 @@ +using System.Security.Principal; + +namespace System.Web.Helpers.AntiXsrf.Test +{ + // An ITokenValidator that can be passed to MoQ + public abstract class MockableTokenValidator : ITokenValidator + { + public abstract object GenerateCookieToken(); + public abstract object GenerateFormToken(HttpContextBase httpContext, IIdentity identity, object cookieToken); + public abstract bool IsCookieTokenValid(object cookieToken); + public abstract void ValidateTokens(HttpContextBase httpContext, IIdentity identity, object cookieToken, object formToken); + + AntiForgeryToken ITokenValidator.GenerateCookieToken() + { + return (AntiForgeryToken)GenerateCookieToken(); + } + + AntiForgeryToken ITokenValidator.GenerateFormToken(HttpContextBase httpContext, IIdentity identity, AntiForgeryToken cookieToken) + { + return (AntiForgeryToken)GenerateFormToken(httpContext, identity, (AntiForgeryToken)cookieToken); + } + + bool ITokenValidator.IsCookieTokenValid(AntiForgeryToken cookieToken) + { + return IsCookieTokenValid((AntiForgeryToken)cookieToken); + } + + void ITokenValidator.ValidateTokens(HttpContextBase httpContext, IIdentity identity, AntiForgeryToken cookieToken, AntiForgeryToken formToken) + { + ValidateTokens(httpContext, identity, (AntiForgeryToken)cookieToken, (AntiForgeryToken)formToken); + } + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/AntiXsrf/TokenValidatorTest.cs b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/TokenValidatorTest.cs new file mode 100644 index 00000000..ecd95a21 --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/AntiXsrf/TokenValidatorTest.cs @@ -0,0 +1,515 @@ +using System.Security.Principal; +using System.Web.Helpers.Test; +using System.Web.Mvc; +using Moq; +using Xunit; +using Xunit.Extensions; +using Assert = Microsoft.TestCommon.AssertEx; + +namespace System.Web.Helpers.AntiXsrf.Test +{ + public class TokenValidatorTest + { + [Fact] + public void GenerateCookieToken() + { + // Arrange + TokenValidator tokenValidator = new TokenValidator( + config: null, + claimUidExtractor: null); + + // Act + AntiForgeryToken retVal = tokenValidator.GenerateCookieToken(); + + // Assert + Assert.NotNull(retVal); + } + + [Fact] + public void GenerateFormToken_AnonymousUser() + { + // Arrange + AntiForgeryToken cookieToken = new AntiForgeryToken() { IsSessionToken = true }; + HttpContextBase httpContext = new Mock<HttpContextBase>().Object; + Mock<IIdentity> mockIdentity = new Mock<IIdentity>(); + mockIdentity.Setup(o => o.IsAuthenticated).Returns(false); + + IAntiForgeryConfig config = new MockAntiForgeryConfig(); + + TokenValidator validator = new TokenValidator( + config: config, + claimUidExtractor: null); + + // Act + var fieldToken = validator.GenerateFormToken(httpContext, mockIdentity.Object, cookieToken); + + // Assert + Assert.NotNull(fieldToken); + Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken); + Assert.False(fieldToken.IsSessionToken); + Assert.Equal("", fieldToken.Username); + Assert.Equal(null, fieldToken.ClaimUid); + Assert.Equal("", fieldToken.AdditionalData); + } + + [Fact] + public void GenerateFormToken_AuthenticatedWithoutUsernameAndNoAdditionalData_NoAdditionalData() + { + // Arrange + AntiForgeryToken cookieToken = new AntiForgeryToken() + { + IsSessionToken = true + }; + + HttpContextBase httpContext = new Mock<HttpContextBase>().Object; + IIdentity identity = new MyAuthenticatedIdentityWithoutUsername(); + IAntiForgeryConfig config = new MockAntiForgeryConfig(); + IClaimUidExtractor claimUidExtractor = new Mock<MockableClaimUidExtractor>().Object; + + TokenValidator validator = new TokenValidator( + config: config, + claimUidExtractor: claimUidExtractor); + + // Act & assert + var ex = Assert.Throws<InvalidOperationException>(() => validator.GenerateFormToken(httpContext, identity, cookieToken)); + Assert.Equal(@"The provided identity of type 'System.Web.Helpers.AntiXsrf.Test.TokenValidatorTest+MyAuthenticatedIdentityWithoutUsername' is marked IsAuthenticated = true but does not have a value for Name. By default, the anti-forgery system requires that all authenticated identities have a unique Name. If it is not possible to provide a unique Name for this identity, consider setting the static property AntiForgeryConfig.AdditionalDataProvider to an instance of a type that can provide some form of unique identifier for the current user.", ex.Message); + } + + [Fact] + public void GenerateFormToken_AuthenticatedWithoutUsernameAndNoAdditionalData_NoAdditionalData_SuppressHeuristics() + { + // Arrange + AntiForgeryToken cookieToken = new AntiForgeryToken() { IsSessionToken = true }; + HttpContextBase httpContext = new Mock<HttpContextBase>().Object; + IIdentity identity = new MyAuthenticatedIdentityWithoutUsername(); + + IAntiForgeryConfig config = new MockAntiForgeryConfig() + { + SuppressIdentityHeuristicChecks = true + }; + IClaimUidExtractor claimUidExtractor = new Mock<MockableClaimUidExtractor>().Object; + + TokenValidator validator = new TokenValidator( + config: config, + claimUidExtractor: claimUidExtractor); + + // Act + var fieldToken = validator.GenerateFormToken(httpContext, identity, cookieToken); + + // Assert + Assert.NotNull(fieldToken); + Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken); + Assert.False(fieldToken.IsSessionToken); + Assert.Equal("", fieldToken.Username); + Assert.Equal(null, fieldToken.ClaimUid); + Assert.Equal("", fieldToken.AdditionalData); + } + + [Fact] + public void GenerateFormToken_AuthenticatedWithoutUsername_WithAdditionalData() + { + // Arrange + AntiForgeryToken cookieToken = new AntiForgeryToken() { IsSessionToken = true }; + HttpContextBase httpContext = new Mock<HttpContextBase>().Object; + IIdentity identity = new MyAuthenticatedIdentityWithoutUsername(); + + Mock<IAntiForgeryAdditionalDataProvider> mockAdditionalDataProvider = new Mock<IAntiForgeryAdditionalDataProvider>(); + mockAdditionalDataProvider.Setup(o => o.GetAdditionalData(httpContext)).Returns("additional-data"); + + IAntiForgeryConfig config = new MockAntiForgeryConfig() + { + AdditionalDataProvider = mockAdditionalDataProvider.Object + }; + IClaimUidExtractor claimUidExtractor = new Mock<MockableClaimUidExtractor>().Object; + + TokenValidator validator = new TokenValidator( + config: config, + claimUidExtractor: claimUidExtractor); + + // Act + var fieldToken = validator.GenerateFormToken(httpContext, identity, cookieToken); + + // Assert + Assert.NotNull(fieldToken); + Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken); + Assert.False(fieldToken.IsSessionToken); + Assert.Equal("", fieldToken.Username); + Assert.Equal(null, fieldToken.ClaimUid); + Assert.Equal("additional-data", fieldToken.AdditionalData); + } + + [Fact] + public void GenerateFormToken_ClaimsBasedIdentity() + { + // Arrange + AntiForgeryToken cookieToken = new AntiForgeryToken() { IsSessionToken = true }; + HttpContextBase httpContext = new Mock<HttpContextBase>().Object; + IIdentity identity = new GenericIdentity("some-identity"); + + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + UniqueClaimTypeIdentifier = "unique-identifier" + }; + + BinaryBlob expectedClaimUid = new BinaryBlob(256); + Mock<MockableClaimUidExtractor> mockClaimUidExtractor = new Mock<MockableClaimUidExtractor>(); + mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(identity)).Returns((object)expectedClaimUid); + + TokenValidator validator = new TokenValidator( + config: config, + claimUidExtractor: mockClaimUidExtractor.Object); + + // Act + var fieldToken = validator.GenerateFormToken(httpContext, identity, cookieToken); + + // Assert + Assert.NotNull(fieldToken); + Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken); + Assert.False(fieldToken.IsSessionToken); + Assert.Equal("", fieldToken.Username); + Assert.Equal(expectedClaimUid, fieldToken.ClaimUid); + Assert.Equal("", fieldToken.AdditionalData); + } + + [Fact] + public void GenerateFormToken_RegularUserWithUsername() + { + // Arrange + AntiForgeryToken cookieToken = new AntiForgeryToken() { IsSessionToken = true }; + + HttpContextBase httpContext = new Mock<HttpContextBase>().Object; + Mock<IIdentity> mockIdentity = new Mock<IIdentity>(); + mockIdentity.Setup(o => o.IsAuthenticated).Returns(true); + mockIdentity.Setup(o => o.Name).Returns("my-username"); + + IAntiForgeryConfig config = new MockAntiForgeryConfig(); + IClaimUidExtractor claimUidExtractor = new Mock<MockableClaimUidExtractor>().Object; + + TokenValidator validator = new TokenValidator( + config: config, + claimUidExtractor: claimUidExtractor); + + // Act + var fieldToken = validator.GenerateFormToken(httpContext, mockIdentity.Object, cookieToken); + + // Assert + Assert.NotNull(fieldToken); + Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken); + Assert.False(fieldToken.IsSessionToken); + Assert.Equal("my-username", fieldToken.Username); + Assert.Equal(null, fieldToken.ClaimUid); + Assert.Equal("", fieldToken.AdditionalData); + } + + [Fact] + public void IsCookieTokenValid_FieldToken_ReturnsFalse() + { + // Arrange + AntiForgeryToken cookieToken = new AntiForgeryToken() + { + IsSessionToken = false + }; + + TokenValidator validator = new TokenValidator( + config: null, + claimUidExtractor: null); + + // Act + bool retVal = validator.IsCookieTokenValid(cookieToken); + + // Assert + Assert.False(retVal); + } + + [Fact] + public void IsCookieTokenValid_NullToken_ReturnsFalse() + { + // Arrange + AntiForgeryToken cookieToken = null; + TokenValidator validator = new TokenValidator( + config: null, + claimUidExtractor: null); + + // Act + bool retVal = validator.IsCookieTokenValid(cookieToken); + + // Assert + Assert.False(retVal); + } + + [Fact] + public void IsCookieTokenValid_ValidToken_ReturnsTrue() + { + // Arrange + AntiForgeryToken cookieToken = new AntiForgeryToken() + { + IsSessionToken = true + }; + + TokenValidator validator = new TokenValidator( + config: null, + claimUidExtractor: null); + + // Act + bool retVal = validator.IsCookieTokenValid(cookieToken); + + // Assert + Assert.True(retVal); + } + + [Fact] + public void ValidateTokens_SessionTokenMissing() + { + // Arrange + HttpContextBase httpContext = new Mock<HttpContextBase>().Object; + IIdentity identity = new Mock<IIdentity>().Object; + AntiForgeryToken sessionToken = null; + AntiForgeryToken fieldtoken = new AntiForgeryToken() { IsSessionToken = false }; + + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + CookieName = "my-cookie-name" + }; + TokenValidator validator = new TokenValidator( + config: config, + claimUidExtractor: null); + + // Act & assert + var ex = Assert.Throws<HttpAntiForgeryException>(() => validator.ValidateTokens(httpContext, identity, sessionToken, fieldtoken)); + Assert.Equal(@"The required anti-forgery cookie ""my-cookie-name"" is not present.", ex.Message); + } + + [Fact] + public void ValidateTokens_FieldTokenMissing() + { + // Arrange + HttpContextBase httpContext = new Mock<HttpContextBase>().Object; + IIdentity identity = new Mock<IIdentity>().Object; + AntiForgeryToken sessionToken = new AntiForgeryToken() { IsSessionToken = true }; + AntiForgeryToken fieldtoken = null; + + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + FormFieldName = "my-form-field-name" + }; + TokenValidator validator = new TokenValidator( + config: config, + claimUidExtractor: null); + + // Act & assert + var ex = Assert.Throws<HttpAntiForgeryException>(() => validator.ValidateTokens(httpContext, identity, sessionToken, fieldtoken)); + Assert.Equal(@"The required anti-forgery form field ""my-form-field-name"" is not present.", ex.Message); + } + + [Fact] + public void ValidateTokens_FieldAndSessionTokensSwapped() + { + // Arrange + HttpContextBase httpContext = new Mock<HttpContextBase>().Object; + IIdentity identity = new Mock<IIdentity>().Object; + AntiForgeryToken sessionToken = new AntiForgeryToken() { IsSessionToken = true }; + AntiForgeryToken fieldtoken = new AntiForgeryToken() { IsSessionToken = false }; + + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + CookieName = "my-cookie-name", + FormFieldName = "my-form-field-name" + }; + TokenValidator validator = new TokenValidator( + config: config, + claimUidExtractor: null); + + // Act & assert + var ex1 = Assert.Throws<HttpAntiForgeryException>(() => validator.ValidateTokens(httpContext, identity, fieldtoken, fieldtoken)); + Assert.Equal(@"Validation of the provided anti-forgery token failed. The cookie ""my-cookie-name"" and the form field ""my-form-field-name"" were swapped.", ex1.Message); + + var ex2 = Assert.Throws<HttpAntiForgeryException>(() => validator.ValidateTokens(httpContext, identity, sessionToken, sessionToken)); + Assert.Equal(@"Validation of the provided anti-forgery token failed. The cookie ""my-cookie-name"" and the form field ""my-form-field-name"" were swapped.", ex2.Message); + } + + [Fact] + public void ValidateTokens_FieldAndSessionTokensHaveDifferentSecurityKeys() + { + // Arrange + HttpContextBase httpContext = new Mock<HttpContextBase>().Object; + IIdentity identity = new Mock<IIdentity>().Object; + AntiForgeryToken sessionToken = new AntiForgeryToken() { IsSessionToken = true }; + AntiForgeryToken fieldtoken = new AntiForgeryToken() { IsSessionToken = false }; + + TokenValidator validator = new TokenValidator( + config: null, + claimUidExtractor: null); + + // Act & assert + var ex = Assert.Throws<HttpAntiForgeryException>(() => validator.ValidateTokens(httpContext, identity, sessionToken, fieldtoken)); + Assert.Equal(@"The anti-forgery cookie token and form field token do not match.", ex.Message); + } + + [Theory] + [InlineData("the-user", "the-other-user")] + [InlineData("http://example.com/uri-casing", "http://example.com/URI-casing")] + [InlineData("https://example.com/secure-uri-casing", "https://example.com/secure-URI-casing")] + public void ValidateTokens_UsernameMismatch(string identityUsername, string embeddedUsername) + { + // Arrange + HttpContextBase httpContext = new Mock<HttpContextBase>().Object; + IIdentity identity = new GenericIdentity(identityUsername); + AntiForgeryToken sessionToken = new AntiForgeryToken() { IsSessionToken = true }; + AntiForgeryToken fieldtoken = new AntiForgeryToken() { SecurityToken = sessionToken.SecurityToken, Username = embeddedUsername, IsSessionToken = false }; + + Mock<MockableClaimUidExtractor> mockClaimUidExtractor = new Mock<MockableClaimUidExtractor>(); + mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(identity)).Returns((object)null); + + TokenValidator validator = new TokenValidator( + config: null, + claimUidExtractor: mockClaimUidExtractor.Object); + + // Act & assert + var ex = Assert.Throws<HttpAntiForgeryException>(() => validator.ValidateTokens(httpContext, identity, sessionToken, fieldtoken)); + Assert.Equal(@"The provided anti-forgery token was meant for user """ + embeddedUsername + @""", but the current user is """ + identityUsername + @""".", ex.Message); + } + + [Fact] + public void ValidateTokens_ClaimUidMismatch() + { + // Arrange + HttpContextBase httpContext = new Mock<HttpContextBase>().Object; + IIdentity identity = new GenericIdentity("the-user"); + AntiForgeryToken sessionToken = new AntiForgeryToken() { IsSessionToken = true }; + AntiForgeryToken fieldtoken = new AntiForgeryToken() { SecurityToken = sessionToken.SecurityToken, IsSessionToken = false, ClaimUid = new BinaryBlob(256) }; + + Mock<MockableClaimUidExtractor> mockClaimUidExtractor = new Mock<MockableClaimUidExtractor>(); + mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(identity)).Returns(new BinaryBlob(256)); + + TokenValidator validator = new TokenValidator( + config: null, + claimUidExtractor: mockClaimUidExtractor.Object); + + // Act & assert + var ex = Assert.Throws<HttpAntiForgeryException>(() => validator.ValidateTokens(httpContext, identity, sessionToken, fieldtoken)); + Assert.Equal(@"The provided anti-forgery token was meant for a different claims-based user than the current user.", ex.Message); + } + + [Fact] + public void ValidateTokens_AdditionalDataRejected() + { + // Arrange + HttpContextBase httpContext = new Mock<HttpContextBase>().Object; + IIdentity identity = new GenericIdentity(String.Empty); + AntiForgeryToken sessionToken = new AntiForgeryToken() { IsSessionToken = true }; + AntiForgeryToken fieldtoken = new AntiForgeryToken() { SecurityToken = sessionToken.SecurityToken, Username = String.Empty, IsSessionToken = false, AdditionalData = "some-additional-data" }; + + Mock<IAntiForgeryAdditionalDataProvider> mockAdditionalDataProvider = new Mock<IAntiForgeryAdditionalDataProvider>(); + mockAdditionalDataProvider.Setup(o => o.ValidateAdditionalData(httpContext, "some-additional-data")).Returns(false); + + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + AdditionalDataProvider = mockAdditionalDataProvider.Object + }; + TokenValidator validator = new TokenValidator( + config: config, + claimUidExtractor: null); + + // Act & assert + var ex = Assert.Throws<HttpAntiForgeryException>(() => validator.ValidateTokens(httpContext, identity, sessionToken, fieldtoken)); + Assert.Equal(@"The provided anti-forgery token failed a custom data check.", ex.Message); + } + + [Fact] + public void ValidateTokens_Success_AnonymousUser() + { + // Arrange + HttpContextBase httpContext = new Mock<HttpContextBase>().Object; + IIdentity identity = new GenericIdentity(String.Empty); + AntiForgeryToken sessionToken = new AntiForgeryToken() { IsSessionToken = true }; + AntiForgeryToken fieldtoken = new AntiForgeryToken() { SecurityToken = sessionToken.SecurityToken, Username = String.Empty, IsSessionToken = false, AdditionalData = "some-additional-data" }; + + Mock<IAntiForgeryAdditionalDataProvider> mockAdditionalDataProvider = new Mock<IAntiForgeryAdditionalDataProvider>(); + mockAdditionalDataProvider.Setup(o => o.ValidateAdditionalData(httpContext, "some-additional-data")).Returns(true); + + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + AdditionalDataProvider = mockAdditionalDataProvider.Object + }; + TokenValidator validator = new TokenValidator( + config: config, + claimUidExtractor: null); + + // Act + validator.ValidateTokens(httpContext, identity, sessionToken, fieldtoken); + + // Assert + // Nothing to assert - if we got this far, success! + } + + [Fact] + public void ValidateTokens_Success_AuthenticatedUserWithUsername() + { + // Arrange + HttpContextBase httpContext = new Mock<HttpContextBase>().Object; + IIdentity identity = new GenericIdentity("the-user"); + AntiForgeryToken sessionToken = new AntiForgeryToken() { IsSessionToken = true }; + AntiForgeryToken fieldtoken = new AntiForgeryToken() { SecurityToken = sessionToken.SecurityToken, Username = "THE-USER", IsSessionToken = false, AdditionalData = "some-additional-data" }; + + Mock<IAntiForgeryAdditionalDataProvider> mockAdditionalDataProvider = new Mock<IAntiForgeryAdditionalDataProvider>(); + mockAdditionalDataProvider.Setup(o => o.ValidateAdditionalData(httpContext, "some-additional-data")).Returns(true); + + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + AdditionalDataProvider = mockAdditionalDataProvider.Object + }; + TokenValidator validator = new TokenValidator( + config: config, + claimUidExtractor: new Mock<MockableClaimUidExtractor>().Object); + + // Act + validator.ValidateTokens(httpContext, identity, sessionToken, fieldtoken); + + // Assert + // Nothing to assert - if we got this far, success! + } + + [Fact] + public void ValidateTokens_Success_ClaimsBasedUser() + { + // Arrange + HttpContextBase httpContext = new Mock<HttpContextBase>().Object; + IIdentity identity = new GenericIdentity("the-user"); + AntiForgeryToken sessionToken = new AntiForgeryToken() { IsSessionToken = true }; + AntiForgeryToken fieldtoken = new AntiForgeryToken() { SecurityToken = sessionToken.SecurityToken, IsSessionToken = false, ClaimUid = new BinaryBlob(256) }; + + Mock<MockableClaimUidExtractor> mockClaimUidExtractor = new Mock<MockableClaimUidExtractor>(); + mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(identity)).Returns(fieldtoken.ClaimUid); + + MockAntiForgeryConfig config = new MockAntiForgeryConfig(); + + TokenValidator validator = new TokenValidator( + config: config, + claimUidExtractor: mockClaimUidExtractor.Object); + + // Act + validator.ValidateTokens(httpContext, identity, sessionToken, fieldtoken); + + // Assert + // Nothing to assert - if we got this far, success! + } + + private sealed class MyAuthenticatedIdentityWithoutUsername : IIdentity + { + public string AuthenticationType + { + get { throw new NotImplementedException(); } + } + + public bool IsAuthenticated + { + get { return true; } + } + + public string Name + { + get { return String.Empty; } + } + } + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/Claims/ClaimTest.cs b/test/System.Web.WebPages.Test/Helpers/Claims/ClaimTest.cs new file mode 100644 index 00000000..a8adea4f --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/Claims/ClaimTest.cs @@ -0,0 +1,62 @@ +using Xunit; +using Assert = Microsoft.TestCommon.AssertEx; + +namespace System.Web.Helpers.Claims.Test +{ + public class ClaimTest + { + [Fact] + public void CtorAndProperties() + { + // Act + Claim claim = new Claim("claim-type", "claim-value"); + + // Assert + Assert.Equal("claim-type", claim.ClaimType); + Assert.Equal("claim-value", claim.Value); + } + + [Fact] + public void Create_WithClaimTypeProperty() + { + // Act + Claim claim = Claim.Create<IClaimType1>(new MyClaimType()); + + // Assert + Assert.Equal("my-claim-type-1", claim.ClaimType); + Assert.Equal("my-claim-value-1", claim.Value); + } + + [Fact] + public void Create_WithTypeProperty() + { + // Act + Claim claim = Claim.Create<IClaimType2>(new MyClaimType()); + + // Assert + Assert.Equal("my-claim-type-2", claim.ClaimType); + Assert.Equal("my-claim-value-2", claim.Value); + } + + private interface IClaimType1 + { + string ClaimType { get; } + string Value { get; } + } + + private interface IClaimType2 + { + string Type { get; } + string Value { get; } + } + + private sealed class MyClaimType : IClaimType1, IClaimType2 + { + string IClaimType1.ClaimType { get { return "my-claim-type-1"; } } + string IClaimType1.Value { get { return "my-claim-value-1"; } } + + string IClaimType2.Type { get { return "my-claim-type-2"; } } + string IClaimType2.Value { get { return "my-claim-value-2"; } } + } + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/Claims/ClaimsIdentityConverterTest.cs b/test/System.Web.WebPages.Test/Helpers/Claims/ClaimsIdentityConverterTest.cs new file mode 100644 index 00000000..365381e3 --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/Claims/ClaimsIdentityConverterTest.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Security.Principal; +using System.Web.Security; +using Moq; +using Xunit; +using Xunit.Extensions; +using Assert = Microsoft.TestCommon.AssertEx; + +namespace System.Web.Helpers.Claims.Test +{ + public class ClaimsIdentityConverterTest + { + [Fact] + public void TryConvert_NoMatches_ReturnsNull() + { + // Arrange + IIdentity identity = new Mock<IIdentity>().Object; + ClaimsIdentityConverter converter = new ClaimsIdentityConverter(new Func<IIdentity, ClaimsIdentity>[0]); + + // Act + ClaimsIdentity retVal = converter.TryConvert(identity); + + // Assert + Assert.Null(retVal); + } + + [Fact] + public void TryConvert_ReturnsFirstMatch() + { + // Arrange + IIdentity identity = new Mock<IIdentity>().Object; + ClaimsIdentity claimsIdentity = new MockClaimsIdentity(); + + ClaimsIdentityConverter converter = new ClaimsIdentityConverter(new Func<IIdentity, ClaimsIdentity>[] + { + _ => null, + i => (i == identity) ? claimsIdentity : null + }); + + // Act + ClaimsIdentity retVal = converter.TryConvert(identity); + + // Assert + Assert.Same(claimsIdentity, retVal); + } + + [Theory] + [GrandfatheredTypesData] + public void TryConvert_SkipsGrandfatheredTypes(IIdentity identity) + { + // Arrange + ClaimsIdentityConverter converter = new ClaimsIdentityConverter(new Func<IIdentity, ClaimsIdentity>[] + { + _ => { throw new Exception("Should never be called."); } + }); + + // Act + ClaimsIdentity retVal = converter.TryConvert(identity); + + // Assert + Assert.Null(retVal); + } + + private sealed class GrandfatheredTypesDataAttribute : DataAttribute + { + // We need to subclass these types so that they implement the + // appropriate interface to be claims-based. + public override IEnumerable<object[]> GetData(MethodInfo methodUnderTest, Type[] parameterTypes) + { + yield return new object[] { new SubclassedFormsIdentity() }; + yield return new object[] { new SubclassedGenericIdentity() }; + + SubclassedWindowsIdentity subclassedWindowsIdentity = null; + using (WindowsIdentity originalIdentity = WindowsIdentity.GetCurrent()) + { + subclassedWindowsIdentity = new SubclassedWindowsIdentity(originalIdentity.Token); + } + yield return new object[] { subclassedWindowsIdentity }; + } + } + + private sealed class SubclassedFormsIdentity : FormsIdentity + { + public SubclassedFormsIdentity() : base(new FormsAuthenticationTicket("my-name", false, 60)) { } + } + + private sealed class SubclassedGenericIdentity : GenericIdentity + { + public SubclassedGenericIdentity() : base("my-name") { } + } + + private sealed class SubclassedWindowsIdentity : WindowsIdentity + { + public SubclassedWindowsIdentity(IntPtr userToken) : base(userToken) { } + } + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/Claims/ClaimsIdentityTest.cs b/test/System.Web.WebPages.Test/Helpers/Claims/ClaimsIdentityTest.cs new file mode 100644 index 00000000..36367b66 --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/Claims/ClaimsIdentityTest.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Principal; +using Xunit; +using Assert = Microsoft.TestCommon.AssertEx; + +namespace System.Web.Helpers.Claims.Test +{ + public class ClaimsIdentityTest + { + [Fact] + public void TryConvert_GetClaims() + { + // Act + ClaimsIdentity claimsIdentity = ClaimsIdentity.TryConvert<IClaimsIdentity, IClaim>(new MyClaimsIdentity()); + var claims = claimsIdentity.GetClaims().ToArray(); + + // Assert + Assert.Equal(2, claims.Length); + Assert.Equal("claim-type-1", claims[0].ClaimType); + Assert.Equal("claim-value-1", claims[0].Value); + Assert.Equal("claim-type-2", claims[1].ClaimType); + Assert.Equal("claim-value-2", claims[1].Value); + } + + private interface IClaimsIdentity : IIdentity + { + IEnumerable<IClaim> Claims { get; } + } + + private interface IClaim + { + string ClaimType { get; } + string Value { get; } + } + + private sealed class MyClaimsIdentity : IClaimsIdentity, IIdentity + { + IEnumerable<IClaim> IClaimsIdentity.Claims + { + get + { + return new MyClaim[] + { + new MyClaim() { ClaimType = "claim-type-1", Value = "claim-value-1" }, + new MyClaim() { ClaimType = "claim-type-2", Value = "claim-value-2" } + }; + } + } + + string IIdentity.AuthenticationType + { + get { throw new NotImplementedException(); } + } + + bool IIdentity.IsAuthenticated + { + get { throw new NotImplementedException(); } + } + + string IIdentity.Name + { + get { throw new NotImplementedException(); } + } + + private sealed class MyClaim : IClaim + { + public string ClaimType { get; set; } + public string Value { get; set; } + } + } + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/Claims/MockClaimsIdentity.cs b/test/System.Web.WebPages.Test/Helpers/Claims/MockClaimsIdentity.cs new file mode 100644 index 00000000..2e52430d --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/Claims/MockClaimsIdentity.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace System.Web.Helpers.Claims.Test +{ + // Convenient class for mocking a ClaimsIdentity instance given some + // prefabricated Claim instances. + internal sealed class MockClaimsIdentity : ClaimsIdentity + { + private readonly List<Claim> _claims = new List<Claim>(); + + public void AddClaim(string claimType, string value) + { + _claims.Add(new Claim(claimType, value)); + } + + public override IEnumerable<Claim> GetClaims() + { + return _claims; + } + } +} diff --git a/test/System.Web.WebPages.Test/Helpers/CryptoUtilTest.cs b/test/System.Web.WebPages.Test/Helpers/CryptoUtilTest.cs new file mode 100644 index 00000000..25a53ea9 --- /dev/null +++ b/test/System.Web.WebPages.Test/Helpers/CryptoUtilTest.cs @@ -0,0 +1,57 @@ +using Xunit; +using Xunit.Extensions; +using Assert = Microsoft.TestCommon.AssertEx; + +namespace System.Web.Helpers.Test +{ + public class CryptoUtilTest + { + [Theory] + [InlineData(new byte[0], null)] + [InlineData(null, new byte[0])] + [InlineData(new byte[0], new byte[] { 0x00 })] + [InlineData(new byte[] { 0x01, 0x02 }, new byte[] { 0x02, 0x01 })] + public void AreByteArraysEqual_False(byte[] a, byte[] b) + { + // Act + bool retVal = CryptoUtil.AreByteArraysEqual(a, b); + + // Assert + Assert.NotEqual(a, b); + } + + [Fact] + public void AreByteArraysEqual_True() + { + // Arrange + byte[] a = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 }; + byte[] b = (byte[])a.Clone(); + + // Act + bool retVal = CryptoUtil.AreByteArraysEqual(a, b); + + // Assert + Assert.Equal(a, b); + } + + [Fact] + public void TestVectors_Empty() + { + // Act + byte[] retVal = CryptoUtil.ComputeSHA256(new string[0]); + + // Assert + Assert.Equal("47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", Convert.ToBase64String(retVal)); + } + + [Fact] + public void TestVectors_NonEmpty() + { + // Act + byte[] retVal = CryptoUtil.ComputeSHA256(new string[] { "a parameter", "another parameter" }); + + // Assert + Assert.Equal("Bez9yYh4Zq9jK1H5jD21wh04HTZi/vgxp6yDE7Y6cfo=", Convert.ToBase64String(retVal)); + } + } +} diff --git a/test/System.Web.WebPages.Test/System.Web.WebPages.Test.csproj b/test/System.Web.WebPages.Test/System.Web.WebPages.Test.csproj index 6ad4e51d..d3828d3c 100644 --- a/test/System.Web.WebPages.Test/System.Web.WebPages.Test.csproj +++ b/test/System.Web.WebPages.Test/System.Web.WebPages.Test.csproj @@ -65,6 +65,27 @@ <Compile Include="ApplicationParts\MimeMappingTest.cs" /> <Compile Include="ApplicationParts\ResourceHandlerTest.cs" /> <Compile Include="ApplicationParts\TestResourceAssembly.cs" /> + <Compile Include="Helpers\AntiXsrf\AntiForgeryTokenStoreTest.cs" /> + <Compile Include="Helpers\AntiXsrf\MockableTokenStore.cs" /> + <Compile Include="Helpers\AntiXsrf\MockableTokenValidator.cs" /> + <Compile Include="Helpers\AntiXsrf\TokenValidatorTest.cs" /> + <Compile Include="Helpers\AntiXsrf\MockableClaimUidExtractor.cs" /> + <Compile Include="Helpers\AntiXsrf\MockableCryptoSystem.cs" /> + <Compile Include="Helpers\AntiXsrf\HexUtil.cs" /> + <Compile Include="Helpers\AntiXsrf\MachineKeyCryptoSystemTest.cs" /> + <Compile Include="Helpers\Claims\ClaimTest.cs" /> + <Compile Include="Helpers\Claims\ClaimsIdentityTest.cs" /> + <Compile Include="Helpers\Claims\ClaimsIdentityConverterTest.cs" /> + <Compile Include="Helpers\AntiXsrf\ClaimUidExtractorTest.cs" /> + <Compile Include="Helpers\AntiXsrf\AntiForgeryTokenTest.cs" /> + <Compile Include="Helpers\AntiXsrf\AntiForgeryTokenSerializerTest.cs" /> + <Compile Include="Helpers\AntiXsrf\AntiForgeryWorkerTest.cs" /> + <Compile Include="Helpers\AntiForgeryConfigTest.cs" /> + <Compile Include="Helpers\AntiXsrf\MockableAntiForgeryTokenSerializer.cs" /> + <Compile Include="Helpers\AntiXsrf\MockAntiForgeryConfig.cs" /> + <Compile Include="Helpers\CryptoUtilTest.cs" /> + <Compile Include="Helpers\Claims\MockClaimsIdentity.cs" /> + <Compile Include="Helpers\AntiXsrf\BinaryBlobTest.cs" /> <Compile Include="Utils\SessionStateUtilTest.cs" /> <Compile Include="WebPage\BrowserHelpersTest.cs" /> <Compile Include="WebPage\BrowserOverrideStoresTest.cs" /> @@ -76,10 +97,7 @@ <Compile Include="Extensions\HttpRequestExtensionsTest.cs" /> <Compile Include="Extensions\StringExtensionsTest.cs" /> <Compile Include="Extensions\HttpResponseExtensionsTest.cs" /> - <Compile Include="Helpers\AntiForgeryDataSerializerTest.cs" /> - <Compile Include="Helpers\AntiForgeryDataTest.cs" /> <Compile Include="Helpers\AntiForgeryTest.cs" /> - <Compile Include="Helpers\AntiForgeryWorkerTest.cs" /> <Compile Include="Helpers\UnvalidatedRequestValuesTest.cs" /> <Compile Include="Html\CheckBoxTest.cs" /> <Compile Include="Html\HtmlHelperFactory.cs" /> |