diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgery.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgery.cs new file mode 100644 index 0000000000..ca7ff68dea --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgery.cs @@ -0,0 +1,98 @@ +using System.ComponentModel; +using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Security.DataProtection; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Provides access to the anti-forgery system, which provides protection against + /// Cross-site Request Forgery (XSRF, also called CSRF) attacks. + /// + public sealed class AntiForgery + { + private static readonly string _purpose = "Microsoft.AspNet.Mvc.AntiXsrf.AntiForgeryToken.v1"; + private readonly AntiForgeryWorker _worker; + + public AntiForgery([NotNull] IClaimUidExtractor claimUidExtractor, + [NotNull] IDataProtectionProvider dataProtectionProvider, + [NotNull] IAntiForgeryAdditionalDataProvider additionalDataProvider) + { + // TODO: This is temporary till we figure out how to flow configs using DI. + var config = new AntiForgeryConfigWrapper(); + var serializer = new AntiForgeryTokenSerializer(dataProtectionProvider.CreateProtector(_purpose)); + var tokenStore = new AntiForgeryTokenStore(config, serializer); + var tokenProvider = new TokenProvider(config, claimUidExtractor, additionalDataProvider); + _worker = new AntiForgeryWorker(serializer, config, tokenStore, tokenProvider, tokenProvider); + } + + /// + /// Generates an anti-forgery token for this request. This token can + /// be validated by calling the Validate() method. + /// + /// The HTTP context associated with the current call. + /// An HTML string corresponding to an <input type="hidden"> + /// element. This element should be put inside a <form>. + /// + /// This method has a side effect: + /// A response cookie is set if there is no valid cookie associated with the request. + /// + public HtmlString GetHtml([NotNull] HttpContext context) + { + TagBuilder builder = _worker.GetFormInputElement(context); + return builder.ToHtmlString(TagRenderMode.SelfClosing); + } + + /// + /// Generates an anti-forgery token pair (cookie and form token) for this request. + /// This method is similar to GetHtml(HttpContext context), but this method gives the caller control + /// over how to persist the returned values. To validate these tokens, call the + /// appropriate overload of Validate. + /// + /// The HTTP context associated with the current call. + /// 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. + /// + /// Unlike the GetHtml(HttpContext context) method, this method has no side effect. The caller + /// is responsible for setting the response cookie and injecting the returned + /// form token as appropriate. + /// + public AntiForgeryTokenSet GetTokens([NotNull] HttpContext context, string oldCookieToken) + { + // 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. + return _worker.GetTokens(context, oldCookieToken); + } + + /// + /// Validates an anti-forgery token that was supplied for this request. + /// The anti-forgery token may be generated by calling GetHtml(HttpContext context). + /// + /// The HTTP context associated with the current call. + public async Task ValidateAsync([NotNull] HttpContext context) + { + await _worker.ValidateAsync(context); + } + + /// + /// Validates an anti-forgery token pair that was generated by the GetTokens method. + /// + /// The HTTP context associated with the current call. + /// The token that was supplied in the request cookie. + /// The token that was supplied in the request form body. + public void Validate([NotNull] HttpContext context, string cookieToken, string formToken) + { + _worker.Validate(context, cookieToken, formToken); + } + + public void Validate([NotNull] HttpContext context, AntiForgeryTokenSet antiForgeryTokenSet) + { + Validate(context, antiForgeryTokenSet.CookieToken, antiForgeryTokenSet.FormToken); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryConfig.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryConfig.cs new file mode 100644 index 0000000000..454a7c4cbe --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryConfig.cs @@ -0,0 +1,64 @@ +namespace Microsoft.AspNet.Mvc +{ + /// + /// Provides programmatic configuration for the anti-forgery token system. + /// + public static class AntiForgeryConfig + { + internal const string AntiForgeryTokenFieldName = "__RequestVerificationToken"; + private static string _cookieName; + + /// + /// Specifies the name of the cookie that is used by the anti-forgery + /// system. + /// + /// + /// If an explicit name is not provided, the system will automatically + /// generate a name. + /// + public static string CookieName + { + get + { + if (_cookieName == null) + { + _cookieName = GetAntiForgeryCookieName(); + } + return _cookieName; + } + set + { + _cookieName = value; + } + } + + /// + /// 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. + /// + public static bool RequireSsl + { + get; + set; + } + + /// + /// Specifies whether to suppress the generation of X-Frame-Options header + /// which is used to prevent ClickJacking. By default, the X-Frame-Options + /// header is generated with the value SAMEORIGIN. If this setting is 'true', + /// the X-Frame-Options header will not be generated for the response. + /// + public static bool SuppressXFrameOptionsHeader + { + get; + set; + } + + // TODO: Replace the stub. + private static string GetAntiForgeryCookieName() + { + return AntiForgeryTokenFieldName; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryConfigWrapper.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryConfigWrapper.cs new file mode 100644 index 0000000000..57dd2b4f7b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryConfigWrapper.cs @@ -0,0 +1,25 @@ +namespace Microsoft.AspNet.Mvc +{ + public sealed class AntiForgeryConfigWrapper : IAntiForgeryConfig + { + public string CookieName + { + get { return AntiForgeryConfig.CookieName; } + } + + public string FormFieldName + { + get { return AntiForgeryConfig.AntiForgeryTokenFieldName; } + } + + public bool RequireSSL + { + get { return AntiForgeryConfig.RequireSsl; } + } + + public bool SuppressXFrameOptionsHeader + { + get { return AntiForgeryConfig.SuppressXFrameOptionsHeader; } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryToken.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryToken.cs new file mode 100644 index 0000000000..b1bb1b58a3 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryToken.cs @@ -0,0 +1,52 @@ +using System; + +namespace Microsoft.AspNet.Mvc +{ + internal sealed class AntiForgeryToken + { + internal const int SecurityTokenBitLength = 128; + internal const int ClaimUidBitLength = 256; + + private string _additionalData = string.Empty; + private string _username = string.Empty; + private BinaryBlob _securityToken; + + public string AdditionalData + { + get { return _additionalData; } + set + { + _additionalData = value ?? string.Empty; + } + } + + 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; } + set + { + _username = value ?? string.Empty; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryTokenSerializer.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryTokenSerializer.cs new file mode 100644 index 0000000000..d64b6e3e7b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryTokenSerializer.cs @@ -0,0 +1,187 @@ + +using System; +using System.IO; +using Microsoft.AspNet.Mvc.Core; +using Microsoft.AspNet.Security.DataProtection; +using System.Text; + +namespace Microsoft.AspNet.Mvc +{ + internal sealed class AntiForgeryTokenSerializer : IAntiForgeryTokenSerializer + { + private readonly IDataProtector _cryptoSystem; + private const byte TokenVersion = 0x01; + + internal AntiForgeryTokenSerializer([NotNull] IDataProtector cryptoSystem) + { + _cryptoSystem = cryptoSystem; + } + + public AntiForgeryToken Deserialize(string serializedToken) + { + Exception innerException = null; + try + { + using (MemoryStream stream = new MemoryStream(UrlTokenDecode(serializedToken))) + { + using (BinaryReader reader = new BinaryReader(stream)) + { + AntiForgeryToken token = DeserializeImpl(reader); + if (token != null) + { + return token; + } + } + } + } + catch(Exception ex) + { + // swallow all exceptions - homogenize error if something went wrong + innerException = ex; + } + + // if we reached this point, something went wrong deserializing + throw new InvalidOperationException(Resources.AntiForgeryToken_DeserializationFailed, innerException); + } + + /* 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 + var embeddedVersion = reader.ReadByte(); + if (embeddedVersion != TokenVersion) + { + return null; + } + + var deserializedToken = new AntiForgeryToken(); + var 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) + { + var 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; + } + + public string Serialize([NotNull] AntiForgeryToken token) + { + using (var stream = new MemoryStream()) + { + using (var 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 UrlTokenEncode(_cryptoSystem.Protect(stream.ToArray())); + } + } + } + + private string UrlTokenEncode(byte[] input) + { + var base64String = Convert.ToBase64String(input); + if (string.IsNullOrEmpty(base64String)) + { + return string.Empty; + } + + var sb = new StringBuilder(); + for (int i = 0; i < base64String.Length; i++) + { + switch (base64String[i]) + { + case '+': + sb.Append('-'); + break; + case '/': + sb.Append('_'); + break; + case '=': + sb.Append('.'); + break; + default: + sb.Append(base64String[i]); + break; + } + } + + return sb.ToString(); + } + + private byte[] UrlTokenDecode(string input) + { + var sb = new StringBuilder(); + for (int i = 0; i < input.Length; i++) + { + switch (input[i]) + { + case '-': + sb.Append('+'); + break; + case '_': + sb.Append('/'); + break; + case '.': + sb.Append('='); + break; + default: + sb.Append(input[i]); + break; + } + } + + return Convert.FromBase64String(sb.ToString()); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryTokenSet.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryTokenSet.cs new file mode 100644 index 0000000000..867847e251 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryTokenSet.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.AspNet.Mvc.Core; + +namespace Microsoft.AspNet.Mvc +{ + public class AntiForgeryTokenSet + { + public AntiForgeryTokenSet(string formToken, string cookieToken) + { + if (string.IsNullOrEmpty(formToken)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, formToken); + } + + FormToken = formToken; + CookieToken = cookieToken; + } + + public string FormToken { get; private set; } + + // The cookie token is allowed to be null. + // This would be the case when the old cookie token is still valid. + // In such cases a call to GetTokens would return a token set with null cookie token. + public string CookieToken { get; private set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryTokenStore.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryTokenStore.cs new file mode 100644 index 0000000000..90c39c95c8 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryTokenStore.cs @@ -0,0 +1,61 @@ + +using System; +using Microsoft.AspNet.Abstractions; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Mvc +{ + // Saves anti-XSRF tokens split between HttpRequest.Cookies and HttpRequest.Form + internal sealed class AntiForgeryTokenStore : ITokenStore + { + private readonly IAntiForgeryConfig _config; + private readonly IAntiForgeryTokenSerializer _serializer; + + internal AntiForgeryTokenStore([NotNull] IAntiForgeryConfig config, + [NotNull] IAntiForgeryTokenSerializer serializer) + { + _config = config; + _serializer = serializer; + } + + public AntiForgeryToken GetCookieToken(HttpContext httpContext) + { + var cookie = httpContext.Request.Cookies[_config.CookieName]; + if (String.IsNullOrEmpty(cookie)) + { + // did not exist + return null; + } + + return _serializer.Deserialize(cookie); + } + + public async Task GetFormTokenAsync(HttpContext httpContext) + { + var form = await httpContext.Request.GetFormAsync(); + string value = form[_config.FormFieldName]; + if (string.IsNullOrEmpty(value)) + { + // did not exist + return null; + } + + return _serializer.Deserialize(value); + } + + public void SaveCookieToken(HttpContext httpContext, AntiForgeryToken token) + { + string serializedToken = _serializer.Serialize(token); + var options = new CookieOptions() { HttpOnly = true }; + + // Note: don't use "newCookie.Secure = _config.RequireSSL;" since the default + // value of newCookie.Secure is poulated out of band. + if (_config.RequireSSL) + { + options.Secure = true; + } + + httpContext.Response.Cookies.Append(_config.CookieName, serializedToken, options); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryWorker.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryWorker.cs new file mode 100644 index 0000000000..437b548815 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryWorker.cs @@ -0,0 +1,212 @@ + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.Mvc.Core; +using Microsoft.AspNet.Mvc.Rendering; + +namespace Microsoft.AspNet.Mvc +{ + internal sealed class AntiForgeryWorker + { + private readonly IAntiForgeryConfig _config; + private readonly IAntiForgeryTokenSerializer _serializer; + private readonly ITokenStore _tokenStore; + private readonly ITokenValidator _validator; + private readonly ITokenGenerator _generator; + + internal AntiForgeryWorker([NotNull] IAntiForgeryTokenSerializer serializer, + [NotNull] IAntiForgeryConfig config, + [NotNull] ITokenStore tokenStore, + [NotNull] ITokenGenerator generator, + [NotNull] ITokenValidator validator) + { + _serializer = serializer; + _config = config; + _tokenStore = tokenStore; + _generator = generator; + _validator = validator; + } + + private void CheckSSLConfig(HttpContext httpContext) + { + if (_config.RequireSSL && !httpContext.Request.IsSecure) + { + throw new InvalidOperationException(Resources.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 ClaimsIdentity ExtractIdentity(HttpContext httpContext) + { + if (httpContext != null) + { + ClaimsPrincipal user = httpContext.User; + + if (user != null) + { + // We only support ClaimsIdentity. + // Todo remove this once httpContext.User moves to ClaimsIdentity. + return user.Identity as ClaimsIdentity; + } + } + + return null; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Caller will just regenerate token in case of failure.")] + private AntiForgeryToken GetCookieTokenNoThrow(HttpContext 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
. This method has a side effect: it may set a response + // cookie. + public TagBuilder GetFormInputElement([NotNull] HttpContext httpContext) + { + CheckSSLConfig(httpContext); + + var oldCookieToken = GetCookieTokenNoThrow(httpContext); + var tokenSet = GetTokens(httpContext, oldCookieToken); + var newCookieToken = tokenSet.CookieToken; + var formToken = tokenSet.FormToken; + if (newCookieToken != null) + { + // If a new cookie was generated, persist it. + _tokenStore.SaveCookieToken(httpContext, newCookieToken); + } + + if (!_config.SuppressXFrameOptionsHeader) + { + // Adding X-Frame-Options header to prevent ClickJacking. See + // http://tools.ietf.org/html/draft-ietf-websec-x-frame-options-10 + // for more information. + httpContext.Response.Headers.Add("X-Frame-Options", new[] { "SAMEORIGIN" }); + } + + // + var 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 AntiForgeryTokenSet GetTokens([NotNull] HttpContext httpContext, string serializedOldCookieToken) + { + CheckSSLConfig(httpContext); + AntiForgeryToken oldCookieToken = DeserializeTokenNoThrow(serializedOldCookieToken); + AntiForgeryToken newCookieToken, formToken; + var tokenSet = GetTokens(httpContext, oldCookieToken); + + var serializedNewCookieToken = Serialize(tokenSet.CookieToken); + var serializedFormToken = Serialize(tokenSet.FormToken); + return new AntiForgeryTokenSet(serializedFormToken, serializedNewCookieToken); + } + + private AntiForgeryTokenSetInternal GetTokens(HttpContext httpContext, AntiForgeryToken oldCookieToken) + { + AntiForgeryToken newCookieToken = null; + if (!_validator.IsCookieTokenValid(oldCookieToken)) + { + // Need to make sure we're always operating with a good cookie token. + oldCookieToken = newCookieToken = _generator.GenerateCookieToken(); + } + + Contract.Assert(_validator.IsCookieTokenValid(oldCookieToken)); + + AntiForgeryToken formToken = _generator. + GenerateFormToken(httpContext, + ExtractIdentity(httpContext), + oldCookieToken); + + return new AntiForgeryTokenSetInternal() + { + CookieToken = newCookieToken, + FormToken = formToken + }; + } + + 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 async Task ValidateAsync([NotNull] HttpContext httpContext) + { + CheckSSLConfig(httpContext); + + // Extract cookie & form tokens + var cookieToken = _tokenStore.GetCookieToken(httpContext); + var formToken = await _tokenStore.GetFormTokenAsync(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([NotNull] HttpContext httpContext, string cookieToken, string formToken) + { + CheckSSLConfig(httpContext); + + // Extract cookie & form tokens + var deserializedCookieToken = DeserializeToken(cookieToken); + var deserializedFormToken = DeserializeToken(formToken); + + // Validate + _validator.ValidateTokens(httpContext, ExtractIdentity(httpContext), deserializedCookieToken, deserializedFormToken); + } + + private class AntiForgeryTokenSetInternal + { + public AntiForgeryToken FormToken { get; set; } + + public AntiForgeryToken CookieToken { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/BinaryBlob.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/BinaryBlob.cs new file mode 100644 index 0000000000..d43c06f496 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/BinaryBlob.cs @@ -0,0 +1,116 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Text; +using Microsoft.AspNet.Security.DataProtection; +using System.Runtime.CompilerServices; + +namespace Microsoft.AspNet.Mvc +{ + // Represents a binary blob (token) that contains random data. + // Useful for binary data inside a serialized stream. + [DebuggerDisplay("{DebuggerString}")] + internal sealed class BinaryBlob : IEquatable + { + 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 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]; + CryptRand.FillBuffer(new ArraySegment(data)); + return data; + } + + // Need to mark it with NoInlining and NoOptimization attributes to ensure that the + // operation runs in constant time. + [MethodImplAttribute(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private 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; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/DefaultAntiForgeryAdditionalDataProvider.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/DefaultAntiForgeryAdditionalDataProvider.cs new file mode 100644 index 0000000000..bb8b3ecb1a --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/DefaultAntiForgeryAdditionalDataProvider.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.Mvc +{ + public class DefaultAntiForgeryAdditionalDataProvider : IAntiForgeryAdditionalDataProvider + { + public virtual string GetAdditionalData(HttpContext context) + { + return string.Empty; + } + + public virtual bool ValidateAdditionalData(HttpContext context, string additionalData) + { + // Default implementation does not understand anything but empty data. + return string.IsNullOrEmpty(additionalData); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/DefaultClaimUidExtractor.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/DefaultClaimUidExtractor.cs new file mode 100644 index 0000000000..f640030c1f --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/DefaultClaimUidExtractor.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Security.Cryptography; + +namespace Microsoft.AspNet.Mvc +{ + // Can extract unique identifers for a claims-based identity + public class DefaultClaimUidExtractor : IClaimUidExtractor + { + public string ExtractClaimUid(ClaimsIdentity claimsIdentity) + { + if (claimsIdentity == null || !claimsIdentity.IsAuthenticated) + { + // Skip anonymous users + return null; + } + + var uniqueIdentifierParameters = GetUniqueIdentifierParameters(claimsIdentity); + byte[] claimUidBytes = ComputeSHA256(uniqueIdentifierParameters); + return Convert.ToBase64String(claimUidBytes); + } + + private static IEnumerable GetUniqueIdentifierParameters(ClaimsIdentity claimsIdentity) + { + // TODO: Need to enable support for special casing acs identities. + var nameIdentifierClaim = claimsIdentity.FindFirst(claim => + String.Equals(ClaimTypes.NameIdentifier, + claim.Type, StringComparison.Ordinal)); + if (nameIdentifierClaim != null && !string.IsNullOrEmpty(nameIdentifierClaim.Value)) + { + return new string[] + { + ClaimTypes.NameIdentifier, + nameIdentifierClaim.Value + }; + } + + // We Do not understand this claimsIdentity, fallback on serializing the entire claims Identity. + var claims = claimsIdentity.Claims.ToList(); + claims.Sort((a, b) => string.Compare(a.Type, b.Type, StringComparison.Ordinal)); + var identifierParameters = new List(); + foreach (var claim in claims) + { + identifierParameters.Add(claim.Type); + identifierParameters.Add(claim.Value); + } + + return identifierParameters; + } + + private static byte[] ComputeSHA256(IEnumerable parameters) + { + using (var ms = new MemoryStream()) + { + using (var bw = new BinaryWriter(ms)) + { + foreach (string parameter in parameters) + { + bw.Write(parameter); // also writes the length as a prefix; unambiguous + } + + bw.Flush(); + + using (var sha256 = SHA256.Create()) + { + byte[] retVal = sha256.ComputeHash(ms.ToArray(), 0, checked((int)ms.Length)); + return retVal; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/IAntiForgeryAdditionalDataProvider.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/IAntiForgeryAdditionalDataProvider.cs new file mode 100644 index 0000000000..49999d8e65 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/IAntiForgeryAdditionalDataProvider.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// 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. + /// + /// + /// The anti-forgery system already embeds the client's username within the + /// generated tokens. This interface provides and consumes supplemental + /// data. If an incoming anti-forgery token contains supplemental data but no + /// additional data provider is configured, the supplemental data will not be + /// validated. + /// + public interface IAntiForgeryAdditionalDataProvider + { + /// + /// Provides additional data to be stored for the anti-forgery tokens generated + /// during this request. + /// + /// Information about the current request. + /// Supplemental data to embed within the anti-forgery token. + string GetAdditionalData(HttpContext context); + + /// + /// Validates additional data that was embedded inside an incoming anti-forgery + /// token. + /// + /// Information about the current request. + /// Supplemental data that was embedded within the token. + /// True if the data is valid; false if the data is invalid. + bool ValidateAdditionalData(HttpContext context, string additionalData); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/IAntiForgeryConfig.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/IAntiForgeryConfig.cs new file mode 100644 index 0000000000..14bc3e8b38 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/IAntiForgeryConfig.cs @@ -0,0 +1,18 @@ +namespace Microsoft.AspNet.Mvc +{ + // Provides configuration information about the anti-forgery system. + public interface IAntiForgeryConfig + { + // 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 X-FRAME-OPTIONS header. + bool SuppressXFrameOptionsHeader { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/IAntiForgeryTokenSerializer.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/IAntiForgeryTokenSerializer.cs new file mode 100644 index 0000000000..eface2f87a --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/IAntiForgeryTokenSerializer.cs @@ -0,0 +1,9 @@ +namespace Microsoft.AspNet.Mvc +{ + // Abstracts out the serialization process for an anti-forgery token + internal interface IAntiForgeryTokenSerializer + { + AntiForgeryToken Deserialize(string serializedToken); + string Serialize(AntiForgeryToken token); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/IClaimUidExtractor.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/IClaimUidExtractor.cs new file mode 100644 index 0000000000..d5414593eb --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/IClaimUidExtractor.cs @@ -0,0 +1,11 @@ +using System.Security.Claims; +using System.Security.Principal; + +namespace Microsoft.AspNet.Mvc +{ + // Can extract unique identifers for a claims-based identity + public interface IClaimUidExtractor + { + string ExtractClaimUid(ClaimsIdentity identity); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/ITokenGenerator.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/ITokenGenerator.cs new file mode 100644 index 0000000000..3e23fe63c6 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/ITokenGenerator.cs @@ -0,0 +1,17 @@ +using System.Security.Principal; +using Microsoft.AspNet.Abstractions; +using System.Security.Claims; + +namespace Microsoft.AspNet.Mvc +{ + // Provides configuration information about the anti-forgery system. + internal interface ITokenGenerator + { + // 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(HttpContext httpContext, ClaimsIdentity identity, AntiForgeryToken cookieToken); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/ITokenStore.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/ITokenStore.cs new file mode 100644 index 0000000000..6026a579e7 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/ITokenStore.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.Mvc +{ + // Provides an abstraction around how tokens are persisted and retrieved for a request + internal interface ITokenStore + { + AntiForgeryToken GetCookieToken(HttpContext httpContext); + Task GetFormTokenAsync(HttpContext httpContext); + void SaveCookieToken(HttpContext httpContext, AntiForgeryToken token); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/ITokenValidator.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/ITokenValidator.cs new file mode 100644 index 0000000000..1146691523 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/ITokenValidator.cs @@ -0,0 +1,17 @@ +using System.Security.Principal; +using Microsoft.AspNet.Abstractions; +using System.Security.Claims; + +namespace Microsoft.AspNet.Mvc +{ + // Provides an abstraction around something that can validate anti-XSRF tokens + internal interface ITokenValidator + { + // 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(HttpContext httpContext, ClaimsIdentity identity, AntiForgeryToken cookieToken, AntiForgeryToken formToken); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/TokenProvider.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/TokenProvider.cs new file mode 100644 index 0000000000..4e65ab6507 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/TokenProvider.cs @@ -0,0 +1,159 @@ +using System; +using System.Diagnostics.Contracts; +using System.Security.Principal; +using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.Mvc.Core; +using System.Security.Claims; + +namespace Microsoft.AspNet.Mvc +{ + internal sealed class TokenProvider : ITokenValidator, ITokenGenerator + { + private readonly IClaimUidExtractor _claimUidExtractor; + private readonly IAntiForgeryConfig _config; + private readonly IAntiForgeryAdditionalDataProvider _additionalDataProvider; + + internal TokenProvider(IAntiForgeryConfig config, + IClaimUidExtractor claimUidExtractor, + IAntiForgeryAdditionalDataProvider additionalDataProvider) + { + _config = config; + _claimUidExtractor = claimUidExtractor; + _additionalDataProvider = additionalDataProvider; + } + + public AntiForgeryToken GenerateCookieToken() + { + return new AntiForgeryToken() + { + // SecurityToken will be populated automatically. + IsSessionToken = true + }; + } + + public AntiForgeryToken GenerateFormToken(HttpContext httpContext, + ClaimsIdentity identity, + AntiForgeryToken cookieToken) + { + Contract.Assert(IsCookieTokenValid(cookieToken)); + + var formToken = new AntiForgeryToken() + { + SecurityToken = cookieToken.SecurityToken, + IsSessionToken = false + }; + + bool isIdentityAuthenticated = false; + + // populate Username and ClaimUid + if (identity != null && identity.IsAuthenticated) + { + isIdentityAuthenticated = true; + formToken.ClaimUid = GetClaimUidBlob(_claimUidExtractor.ExtractClaimUid(identity)); + if (formToken.ClaimUid == null) + { + formToken.Username = identity.Name; + } + } + + // populate AdditionalData + if (_additionalDataProvider != null) + { + formToken.AdditionalData = _additionalDataProvider.GetAdditionalData(httpContext); + } + + if (isIdentityAuthenticated + && 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( + Resources. + FormatTokenValidator_AuthenticatedUserWithoutUsername(identity.GetType())); + } + + return formToken; + } + + public bool IsCookieTokenValid(AntiForgeryToken cookieToken) + { + return (cookieToken != null && cookieToken.IsSessionToken); + } + + public void ValidateTokens(HttpContext httpContext, ClaimsIdentity identity, AntiForgeryToken sessionToken, AntiForgeryToken fieldToken) + { + // Were the tokens even present at all? + if (sessionToken == null) + { + throw new InvalidOperationException(Resources.FormatAntiForgeryToken_CookieMissing(_config.CookieName)); + } + if (fieldToken == null) + { + throw new InvalidOperationException(Resources.FormatAntiForgeryToken_FormFieldMissing(_config.FormFieldName)); + } + + // Do the tokens have the correct format? + if (!sessionToken.IsSessionToken || fieldToken.IsSessionToken) + { + throw new InvalidOperationException(Resources.FormatAntiForgeryToken_TokensSwapped(_config.CookieName, _config.FormFieldName)); + } + + // Are the security tokens embedded in each incoming token identical? + if (!Equals(sessionToken.SecurityToken, fieldToken.SecurityToken)) + { + throw new InvalidOperationException(Resources.AntiForgeryToken_SecurityTokenMismatch); + } + + // Is the incoming token meant for the current user? + string currentUsername = string.Empty; + BinaryBlob currentClaimUid = null; + + if (identity != null && identity.IsAuthenticated) + { + currentClaimUid = GetClaimUidBlob(_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 new InvalidOperationException(Resources. + FormatAntiForgeryToken_UsernameMismatch(fieldToken.Username, + currentUsername)); + } + + if (!Equals(fieldToken.ClaimUid, currentClaimUid)) + { + throw new InvalidOperationException(Resources.AntiForgeryToken_ClaimUidMismatch); + } + + // Is the AdditionalData valid? + if (_additionalDataProvider != null && !_additionalDataProvider.ValidateAdditionalData(httpContext, fieldToken.AdditionalData)) + { + throw new InvalidOperationException(Resources.AntiForgeryToken_AdditionalDataCheckFailed); + } + } + + private static BinaryBlob GetClaimUidBlob(string base64ClaimUid) + { + if (base64ClaimUid == null) + { + return null; + } + + return new BinaryBlob(256, Convert.FromBase64String(base64ClaimUid)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj index 8b94db3a49..2325f7c1ac 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj +++ b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj @@ -40,6 +40,25 @@ + + + + + + + + + + + + + + + + + + + @@ -112,6 +131,7 @@ + diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index f6b97780d3..3608028c9b 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -10,6 +10,54 @@ internal static class Resources private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.AspNet.Mvc.Core.Resources", typeof(Resources).GetTypeInfo().Assembly); + /// + /// 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 extending IAdditionalDataProvider by overriding the DefaultAdditionalDataProvider or a custom type that can provide some form of unique identifier for the current user. + /// + internal static string TokenValidator_AuthenticatedUserWithoutUsername + { + get { return GetString("TokenValidator_AuthenticatedUserWithoutUsername"); } + } + + /// + /// 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 extending IAdditionalDataProvider by overriding the DefaultAdditionalDataProvider or a custom type that can provide some form of unique identifier for the current user. + /// + internal static string FormatTokenValidator_AuthenticatedUserWithoutUsername(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TokenValidator_AuthenticatedUserWithoutUsername"), p0); + } + + /// + /// A claim of type '{0}' was not present on the provided ClaimsIdentity. + /// + internal static string ClaimUidExtractor_ClaimNotPresent + { + get { return GetString("ClaimUidExtractor_ClaimNotPresent"); } + } + + /// + /// A claim of type '{0}' was not present on the provided ClaimsIdentity. + /// + internal static string FormatClaimUidExtractor_ClaimNotPresent(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ClaimUidExtractor_ClaimNotPresent"), p0); + } + + /// + /// 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. + /// + internal static string ClaimUidExtractor_DefaultClaimsNotPresent + { + get { return GetString("ClaimUidExtractor_DefaultClaimsNotPresent"); } + } + + /// + /// 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. + /// + internal static string FormatClaimUidExtractor_DefaultClaimsNotPresent() + { + return GetString("ClaimUidExtractor_DefaultClaimsNotPresent"); + } + /// /// The method '{0}' on type '{1}' returned an instance of '{2}'. Make sure to call Unwrap on the returned value to avoid unobserved faulted Task. /// @@ -42,6 +90,150 @@ internal static string FormatActionExecutor_UnexpectedTaskInstance(object p0, ob return string.Format(CultureInfo.CurrentCulture, GetString("ActionExecutor_UnexpectedTaskInstance"), p0, p1); } + /// + /// The provided anti-forgery token failed a custom data check. + /// + internal static string AntiForgeryToken_AdditionalDataCheckFailed + { + get { return GetString("AntiForgeryToken_AdditionalDataCheckFailed"); } + } + + /// + /// The provided anti-forgery token failed a custom data check. + /// + internal static string FormatAntiForgeryToken_AdditionalDataCheckFailed() + { + return GetString("AntiForgeryToken_AdditionalDataCheckFailed"); + } + + /// + /// The provided anti-forgery token was meant for a different claims-based user than the current user. + /// + internal static string AntiForgeryToken_ClaimUidMismatch + { + get { return GetString("AntiForgeryToken_ClaimUidMismatch"); } + } + + /// + /// The provided anti-forgery token was meant for a different claims-based user than the current user. + /// + internal static string FormatAntiForgeryToken_ClaimUidMismatch() + { + return GetString("AntiForgeryToken_ClaimUidMismatch"); + } + + /// + /// The required anti-forgery cookie "{0}" is not present. + /// + internal static string AntiForgeryToken_CookieMissing + { + get { return GetString("AntiForgeryToken_CookieMissing"); } + } + + /// + /// The required anti-forgery cookie "{0}" is not present. + /// + internal static string FormatAntiForgeryToken_CookieMissing(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("AntiForgeryToken_CookieMissing"), p0); + } + + /// + /// 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. + /// + internal static string AntiForgeryToken_DeserializationFailed + { + get { return GetString("AntiForgeryToken_DeserializationFailed"); } + } + + /// + /// 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. + /// + internal static string FormatAntiForgeryToken_DeserializationFailed() + { + return GetString("AntiForgeryToken_DeserializationFailed"); + } + + /// + /// The required anti-forgery form field "{0}" is not present. + /// + internal static string AntiForgeryToken_FormFieldMissing + { + get { return GetString("AntiForgeryToken_FormFieldMissing"); } + } + + /// + /// The required anti-forgery form field "{0}" is not present. + /// + internal static string FormatAntiForgeryToken_FormFieldMissing(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("AntiForgeryToken_FormFieldMissing"), p0); + } + + /// + /// The anti-forgery cookie token and form field token do not match. + /// + internal static string AntiForgeryToken_SecurityTokenMismatch + { + get { return GetString("AntiForgeryToken_SecurityTokenMismatch"); } + } + + /// + /// The anti-forgery cookie token and form field token do not match. + /// + internal static string FormatAntiForgeryToken_SecurityTokenMismatch() + { + return GetString("AntiForgeryToken_SecurityTokenMismatch"); + } + + /// + /// Validation of the provided anti-forgery token failed. The cookie "{0}" and the form field "{1}" were swapped. + /// + internal static string AntiForgeryToken_TokensSwapped + { + get { return GetString("AntiForgeryToken_TokensSwapped"); } + } + + /// + /// Validation of the provided anti-forgery token failed. The cookie "{0}" and the form field "{1}" were swapped. + /// + internal static string FormatAntiForgeryToken_TokensSwapped(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("AntiForgeryToken_TokensSwapped"), p0, p1); + } + + /// + /// The provided anti-forgery token was meant for user "{0}", but the current user is "{1}". + /// + internal static string AntiForgeryToken_UsernameMismatch + { + get { return GetString("AntiForgeryToken_UsernameMismatch"); } + } + + /// + /// The provided anti-forgery token was meant for user "{0}", but the current user is "{1}". + /// + internal static string FormatAntiForgeryToken_UsernameMismatch(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("AntiForgeryToken_UsernameMismatch"), p0, p1); + } + + /// + /// The anti-forgery system has the configuration value AntiForgeryConfig.RequireSsl = true, but the current request is not an SSL request. + /// + internal static string AntiForgeryWorker_RequireSSL + { + get { return GetString("AntiForgeryWorker_RequireSSL"); } + } + + /// + /// The anti-forgery system has the configuration value AntiForgeryConfig.RequireSsl = true, but the current request is not an SSL request. + /// + internal static string FormatAntiForgeryWorker_RequireSSL() + { + return GetString("AntiForgeryWorker_RequireSSL"); + } + /// /// The class ReflectedActionFilterEndPoint only supports ReflectedActionDescriptors. /// diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs index 72f3938a12..f5df17d95b 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs @@ -30,6 +30,7 @@ public class HtmlHelper : ICanHasViewContext private readonly IUrlHelper _urlHelper; private readonly IViewEngine _viewEngine; + private readonly AntiForgery _antiForgeryInstance; private ViewContext _viewContext; @@ -39,11 +40,13 @@ public class HtmlHelper : ICanHasViewContext public HtmlHelper( [NotNull] IViewEngine viewEngine, [NotNull] IModelMetadataProvider metadataProvider, - [NotNull] IUrlHelper urlHelper) + [NotNull] IUrlHelper urlHelper, + [NotNull] AntiForgery antiForgeryInstance) { _viewEngine = viewEngine; MetadataProvider = metadataProvider; _urlHelper = urlHelper; + _antiForgeryInstance = antiForgeryInstance; // Underscores are fine characters in id's. IdAttributeDotReplacement = "_"; @@ -158,6 +161,11 @@ public virtual void Contextualize([NotNull] ViewContext viewContext) ViewContext = viewContext; } + public HtmlString AntiForgeryToken() + { + return _antiForgeryInstance.GetHtml(ViewContext.HttpContext); + } + public MvcForm BeginForm(string actionName, string controllerName, object routeValues, FormMethod method, object htmlAttributes) { diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelperOfT.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelperOfT.cs index c2af9e14c2..5f1e5c431f 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelperOfT.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelperOfT.cs @@ -15,8 +15,9 @@ public class HtmlHelper : HtmlHelper, IHtmlHelper public HtmlHelper( [NotNull] IViewEngine viewEngine, [NotNull] IModelMetadataProvider metadataProvider, - [NotNull] IUrlHelper urlHelper) - : base(viewEngine, metadataProvider, urlHelper) + [NotNull] IUrlHelper urlHelper, + [NotNull] AntiForgery antiForgeryInstance) + : base(viewEngine, metadataProvider, urlHelper, antiForgeryInstance) { } diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelperOfT.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelperOfT.cs index 07d11c07af..3419627605 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelperOfT.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/IHtmlHelperOfT.cs @@ -61,7 +61,14 @@ HtmlString ActionLink( string fragment, object routeValues, object htmlAttributes); - + + /// + /// Generates a hidden form field (anti-forgery token) that is validated when the form is submitted. + /// + /// + /// The generated form field (anti-forgery token). + /// + HtmlString AntiForgeryToken(); /// /// Writes an opening tag to the response. When the user submits the form, diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index 2f6151a79f..f7e38e0ced 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -117,12 +117,48 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + The provided anti-forgery token failed a custom data check. + + + The provided anti-forgery token was meant for a different claims-based user than the current user. + + + The required anti-forgery cookie "{0}" is not present. + + + 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. + + + The required anti-forgery form field "{0}" is not present. + + + The anti-forgery cookie token and form field token do not match. + + + Validation of the provided anti-forgery token failed. The cookie "{0}" and the form field "{1}" were swapped. + + + The provided anti-forgery token was meant for user "{0}", but the current user is "{1}". + + + The anti-forgery system has the configuration value AntiForgeryConfig.RequireSsl = true, but the current request is not an SSL request. + The method '{0}' on type '{1}' returned an instance of '{2}'. Make sure to call Unwrap on the returned value to avoid unobserved faulted Task. The method '{0}' on type '{1}' returned a Task instance even though it is not an asynchronous method. + + + A claim of type '{0}' was not present on the provided ClaimsIdentity. + + + 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 extending IAdditionalDataProvider by overriding the DefaultAdditionalDataProvider or a custom type that can provide some form of unique identifier for the current user. + The class ReflectedActionFilterEndPoint only supports ReflectedActionDescriptors. diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json index 30481e9172..85b6e3fd28 100644 --- a/src/Microsoft.AspNet.Mvc.Core/project.json +++ b/src/Microsoft.AspNet.Mvc.Core/project.json @@ -7,7 +7,8 @@ "Microsoft.AspNet.Routing": "0.1-alpha-*", "Common": "", "Microsoft.AspNet.Mvc.ModelBinding": "", - "Microsoft.Net.Runtime.Interfaces": "0.1-alpha-*" + "Microsoft.Net.Runtime.Interfaces": "0.1-alpha-*", + "Microsoft.AspNet.Security.DataProtection" : "0.1-alpha-*" }, "configurations": { "net45": {}, @@ -33,6 +34,9 @@ "System.Runtime": "4.0.20.0", "System.Runtime.Extensions": "4.0.10.0", "System.Runtime.InteropServices": "4.0.20.0", + "System.Security.Cryptography": "4.0.0.0", + "System.Security.Cryptography.HashAlgorithms.SHA2": "4.0.0.0", + "System.Security.Principal": "4.0.0.0", "System.Text.Encoding": "4.0.20.0", "System.Threading": "4.0.0.0", "System.Threading.Tasks": "4.0.10.0" diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataMemberModelValidatorProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataMemberModelValidatorProvider.cs index 5784518422..cb6c7631d6 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataMemberModelValidatorProvider.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataMemberModelValidatorProvider.cs @@ -47,4 +47,4 @@ internal static bool IsRequiredDataMember(Type containerType, IEnumerable GetDefaultServices(IConfiguration yield return describe.Transient(); yield return describe.Transient(); + yield return describe.Singleton(); + yield return describe.Singleton(); + yield return describe.Singleton(); + yield return describe.Describe( typeof(INestedProviderManager<>),