From 3dc2663c3509a5c3c249e383763c5492338528f9 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Mon, 8 Jun 2015 14:28:39 -0700 Subject: [PATCH] Code dump of Antiforgery code --- .../Antiforgery.cs | 139 +++++ .../AntiforgeryContext.cs | 13 + .../AntiforgeryContextAccessor.cs | 10 + .../AntiforgeryOptions.cs | 81 +++ .../AntiforgeryToken.cs | 53 ++ .../AntiforgeryTokenProvider.cs | 168 +++++ .../AntiforgeryTokenSerializer.cs | 135 ++++ .../AntiforgeryTokenSet.cs | 42 ++ .../AntiforgeryTokenStore.cs | 79 +++ .../BinaryBlob.cs | 117 ++++ ...efaultAntiforgeryAdditionalDataProvider.cs | 26 + .../DefaultClaimUidExtractor.cs | 82 +++ .../IAntiforgeryAdditionalDataProvider.cs | 39 ++ .../IAntiforgeryContextAccessor.cs | 10 + .../IAntiforgeryTokenGenerator.cs | 22 + .../IAntiforgeryTokenSerializer.cs | 12 + .../IAntiforgeryTokenStore.cs | 16 + .../IAntiforgeryTokenValidator.cs | 23 + .../IClaimUidExtractor.cs | 20 + .../Internal/AntiforgeryWorker.cs | 251 ++++++++ .../Properties/AssemblyInfo.cs | 9 + .../Properties/Resources.Designer.cs | 206 ++++++ .../Resources.resx | 153 +++++ src/Microsoft.AspNet.Antiforgery/project.json | 32 +- .../AntiforgeryTokenSerializerTest.cs | 184 ++++++ .../AntiforgeryTokenStoreTest.cs | 428 +++++++++++++ .../AntiforgeryTokenTest.cs | 132 ++++ .../AntiforgeryWorkerTest.cs | 583 +++++++++++++++++ .../BinaryBlobTest.cs | 129 ++++ .../ClaimUidExtractorTest.cs | 115 ++++ .../Microsoft.AspNet.Antiforgery.Test.xproj | 7 +- .../TokenProviderTest.cs | 588 ++++++++++++++++++ .../project.json | 9 +- 33 files changed, 3897 insertions(+), 16 deletions(-) create mode 100644 src/Microsoft.AspNet.Antiforgery/Antiforgery.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/AntiforgeryContext.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/AntiforgeryContextAccessor.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/AntiforgeryOptions.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/AntiforgeryToken.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/AntiforgeryTokenProvider.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/AntiforgeryTokenSerializer.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/AntiforgeryTokenSet.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/AntiforgeryTokenStore.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/BinaryBlob.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/DefaultAntiforgeryAdditionalDataProvider.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/DefaultClaimUidExtractor.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/IAntiforgeryAdditionalDataProvider.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/IAntiforgeryContextAccessor.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/IAntiforgeryTokenGenerator.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/IAntiforgeryTokenSerializer.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/IAntiforgeryTokenStore.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/IAntiforgeryTokenValidator.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/IClaimUidExtractor.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/Internal/AntiforgeryWorker.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/Properties/Resources.Designer.cs create mode 100644 src/Microsoft.AspNet.Antiforgery/Resources.resx create mode 100644 test/Microsoft.AspNet.Antiforgery.Test/AntiforgeryTokenSerializerTest.cs create mode 100644 test/Microsoft.AspNet.Antiforgery.Test/AntiforgeryTokenStoreTest.cs create mode 100644 test/Microsoft.AspNet.Antiforgery.Test/AntiforgeryTokenTest.cs create mode 100644 test/Microsoft.AspNet.Antiforgery.Test/AntiforgeryWorkerTest.cs create mode 100644 test/Microsoft.AspNet.Antiforgery.Test/BinaryBlobTest.cs create mode 100644 test/Microsoft.AspNet.Antiforgery.Test/ClaimUidExtractorTest.cs create mode 100644 test/Microsoft.AspNet.Antiforgery.Test/TokenProviderTest.cs diff --git a/src/Microsoft.AspNet.Antiforgery/Antiforgery.cs b/src/Microsoft.AspNet.Antiforgery/Antiforgery.cs new file mode 100644 index 0000000000..ab2d2b7c31 --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/Antiforgery.cs @@ -0,0 +1,139 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Antiforgery.Internal; +using Microsoft.AspNet.DataProtection; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.WebUtilities; +using Microsoft.Framework.Internal; +using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.WebEncoders; + +namespace Microsoft.AspNet.Antiforgery +{ + /// + /// 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.Antiforgery.AntiforgeryToken.v1"; + private readonly AntiforgeryWorker _worker; + + public Antiforgery( + [NotNull] IClaimUidExtractor claimUidExtractor, + [NotNull] IDataProtectionProvider dataProtectionProvider, + [NotNull] IAntiforgeryAdditionalDataProvider additionalDataProvider, + [NotNull] IOptions AntiforgeryOptionsAccessor, + [NotNull] IHtmlEncoder htmlEncoder, + [NotNull] IOptions dataProtectionOptions) + { + var AntiforgeryOptions = AntiforgeryOptionsAccessor.Options; + var applicationId = dataProtectionOptions.Options.ApplicationDiscriminator ?? string.Empty; + AntiforgeryOptions.CookieName = AntiforgeryOptions.CookieName ?? ComputeCookieName(applicationId); + + var serializer = new AntiforgeryTokenSerializer(dataProtectionProvider.CreateProtector(_purpose)); + var tokenStore = new AntiforgeryTokenStore(AntiforgeryOptions, serializer); + var tokenProvider = new AntiforgeryTokenProvider(AntiforgeryOptions, claimUidExtractor, additionalDataProvider); + _worker = new AntiforgeryWorker(serializer, AntiforgeryOptions, tokenStore, tokenProvider, tokenProvider, htmlEncoder); + } + + /// + /// 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 string GetHtml([NotNull] HttpContext context) + { + var html = _worker.GetFormInputElement(context); + return html; + } + + /// + /// 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); + } + + /// + /// Validates an anti-forgery token pair that was generated by the GetTokens method. + /// + /// The HTTP context associated with the current call. + /// The anti-forgery token pair (cookie and form token) for this request. + /// + public void Validate([NotNull] HttpContext context, AntiforgeryTokenSet AntiforgeryTokenSet) + { + Validate(context, AntiforgeryTokenSet.CookieToken, AntiforgeryTokenSet.FormToken); + } + + /// + /// Generates and sets an anti-forgery cookie if one is not available or not valid. Also sets response headers. + /// + /// The HTTP context associated with the current call. + public void SetCookieTokenAndHeader([NotNull] HttpContext context) + { + _worker.SetCookieTokenAndHeader(context); + } + + private string ComputeCookieName(string applicationId) + { + using (var sha256 = SHA256.Create()) + { + var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(applicationId)); + var subHash = hash.Take(8).ToArray(); + return WebEncoders.Base64UrlEncode(subHash); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Antiforgery/AntiforgeryContext.cs b/src/Microsoft.AspNet.Antiforgery/AntiforgeryContext.cs new file mode 100644 index 0000000000..ded759255c --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/AntiforgeryContext.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Antiforgery +{ + /// + /// Used as a per request state. + /// + public class AntiforgeryContext + { + public AntiforgeryToken CookieToken { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Antiforgery/AntiforgeryContextAccessor.cs b/src/Microsoft.AspNet.Antiforgery/AntiforgeryContextAccessor.cs new file mode 100644 index 0000000000..3a2806a10f --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/AntiforgeryContextAccessor.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Antiforgery +{ + public class AntiforgeryContextAccessor : IAntiforgeryContextAccessor + { + public AntiforgeryContext Value { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Antiforgery/AntiforgeryOptions.cs b/src/Microsoft.AspNet.Antiforgery/AntiforgeryOptions.cs new file mode 100644 index 0000000000..50f8f42b51 --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/AntiforgeryOptions.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Antiforgery +{ + /// + /// Provides programmatic configuration for the anti-forgery token system. + /// + public class AntiforgeryOptions + { + private const string AntiforgeryTokenFieldName = "__RequestVerificationToken"; + + private string _cookieName; + private string _formFieldName = AntiforgeryTokenFieldName; + + /// + /// 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 string CookieName + { + get + { + return _cookieName; + } + + [param: NotNull] + set + { + _cookieName = value; + } + } + + /// + /// Specifies the name of the anti-forgery token field that is used by the anti-forgery system. + /// + public string FormFieldName + { + get + { + return _formFieldName; + } + + [param: NotNull] + set + { + _formFieldName = 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 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 bool SuppressXFrameOptionsHeader + { + get; + set; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Antiforgery/AntiforgeryToken.cs b/src/Microsoft.AspNet.Antiforgery/AntiforgeryToken.cs new file mode 100644 index 0000000000..61f9665c26 --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/AntiforgeryToken.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Antiforgery +{ + public 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.Antiforgery/AntiforgeryTokenProvider.cs b/src/Microsoft.AspNet.Antiforgery/AntiforgeryTokenProvider.cs new file mode 100644 index 0000000000..cdb02b986f --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/AntiforgeryTokenProvider.cs @@ -0,0 +1,168 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Security.Claims; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Antiforgery +{ + public sealed class AntiforgeryTokenProvider : IAntiforgeryTokenValidator, IAntiforgeryTokenGenerator + { + private readonly IClaimUidExtractor _claimUidExtractor; + private readonly AntiforgeryOptions _config; + private readonly IAntiforgeryAdditionalDataProvider _additionalDataProvider; + + internal AntiforgeryTokenProvider( + AntiforgeryOptions 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) + { + Debug.Assert(IsCookieTokenValid(cookieToken)); + + var formToken = new AntiforgeryToken() + { + SecurityToken = cookieToken.SecurityToken, + IsSessionToken = false + }; + + var 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.FormatAntiforgeryTokenValidator_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? + var 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. + var 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.Antiforgery/AntiforgeryTokenSerializer.cs b/src/Microsoft.AspNet.Antiforgery/AntiforgeryTokenSerializer.cs new file mode 100644 index 0000000000..9022f77b13 --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/AntiforgeryTokenSerializer.cs @@ -0,0 +1,135 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using Microsoft.AspNet.DataProtection; +using Microsoft.AspNet.WebUtilities; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Antiforgery +{ + public sealed class AntiforgeryTokenSerializer : IAntiforgeryTokenSerializer + { + private readonly IDataProtector _cryptoSystem; + private const byte TokenVersion = 0x01; + + public AntiforgeryTokenSerializer([NotNull] IDataProtector cryptoSystem) + { + _cryptoSystem = cryptoSystem; + } + + public AntiforgeryToken Deserialize(string serializedToken) + { + Exception innerException = null; + try + { + var tokenBytes = WebEncoders.Base64UrlDecode(serializedToken); + using (var stream = new MemoryStream(_cryptoSystem.Unprotect(tokenBytes))) + { + using (var reader = new BinaryReader(stream)) + { + var 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) + { + var 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 WebEncoders.Base64UrlEncode(_cryptoSystem.Protect(stream.ToArray())); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Antiforgery/AntiforgeryTokenSet.cs b/src/Microsoft.AspNet.Antiforgery/AntiforgeryTokenSet.cs new file mode 100644 index 0000000000..c18e15dd4f --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/AntiforgeryTokenSet.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Antiforgery +{ + /// + /// The anti-forgery token pair (cookie and form token) for a request. + /// + public class AntiforgeryTokenSet + { + /// + /// Creates the anti-forgery token pair (cookie and form token) for a request. + /// + /// The token that is supplied in the request form body. + /// The token that is supplied in the request cookie. + public AntiforgeryTokenSet(string formToken, string cookieToken) + { + if (string.IsNullOrEmpty(formToken)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(formToken)); + } + + FormToken = formToken; + + // Cookie Token is allowed to be null in the case when the old cookie is valid + // and there is no new cookieToken generated. + CookieToken = cookieToken; + } + + /// + /// The token that is supplied in the request form body. + /// + 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.Antiforgery/AntiforgeryTokenStore.cs b/src/Microsoft.AspNet.Antiforgery/AntiforgeryTokenStore.cs new file mode 100644 index 0000000000..e288c1c786 --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/AntiforgeryTokenStore.cs @@ -0,0 +1,79 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Antiforgery +{ + // Saves anti-XSRF tokens split between HttpRequest.Cookies and HttpRequest.Form + public sealed class AntiforgeryTokenStore : IAntiforgeryTokenStore + { + private readonly AntiforgeryOptions _config; + private readonly IAntiforgeryTokenSerializer _serializer; + + public AntiforgeryTokenStore([NotNull] AntiforgeryOptions config, + [NotNull] IAntiforgeryTokenSerializer serializer) + { + _config = config; + _serializer = serializer; + } + + public AntiforgeryToken GetCookieToken(HttpContext httpContext) + { + var contextAccessor = + httpContext.RequestServices.GetRequiredService(); + if (contextAccessor.Value != null) + { + return contextAccessor.Value.CookieToken; + } + + var requestCookie = httpContext.Request.Cookies[_config.CookieName]; + if (string.IsNullOrEmpty(requestCookie)) + { + // unable to find the cookie. + return null; + } + + return _serializer.Deserialize(requestCookie); + } + + public async Task GetFormTokenAsync(HttpContext httpContext) + { + var form = await httpContext.Request.ReadFormAsync(); + var value = form[_config.FormFieldName]; + if (string.IsNullOrEmpty(value)) + { + // did not exist + return null; + } + + return _serializer.Deserialize(value); + } + + public void SaveCookieToken(HttpContext httpContext, AntiforgeryToken token) + { + // Add the cookie to the request based context. + // This is useful if the cookie needs to be reloaded in the context of the same request. + var contextAccessor = + httpContext.RequestServices.GetRequiredService(); + Debug.Assert(contextAccessor.Value == null, "AntiforgeryContext should be set only once per request."); + contextAccessor.Value = new AntiforgeryContext() { CookieToken = token }; + + var 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.Antiforgery/BinaryBlob.cs b/src/Microsoft.AspNet.Antiforgery/BinaryBlob.cs new file mode 100644 index 0000000000..e0faebed0a --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/BinaryBlob.cs @@ -0,0 +1,117 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; + +namespace Microsoft.AspNet.Antiforgery +{ + // Represents a binary blob (token) that contains random data. + // Useful for binary data inside a serialized stream. + [DebuggerDisplay("{DebuggerString}")] + public sealed class BinaryBlob : IEquatable + { + private static readonly RandomNumberGenerator _randomNumberGenerator = RandomNumberGenerator.Create(); + 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); + } + } + + private string DebuggerString + { + get + { + var sb = new StringBuilder("0x", 2 + (_data.Length * 2)); + for (var 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; + } + + Debug.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. + Debug.Assert(_data != null && _data.Length >= (32 / 8)); + return BitConverter.ToInt32(_data, 0); + } + + private static byte[] GenerateNewToken(int bitLength) + { + var data = new byte[bitLength / 8]; + _randomNumberGenerator.GetBytes(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; + } + + var areEqual = true; + for (var 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.Antiforgery/DefaultAntiforgeryAdditionalDataProvider.cs b/src/Microsoft.AspNet.Antiforgery/DefaultAntiforgeryAdditionalDataProvider.cs new file mode 100644 index 0000000000..2d5967f368 --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/DefaultAntiforgeryAdditionalDataProvider.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Antiforgery +{ + /// + /// A default implementation. + /// + 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.Antiforgery/DefaultClaimUidExtractor.cs b/src/Microsoft.AspNet.Antiforgery/DefaultClaimUidExtractor.cs new file mode 100644 index 0000000000..7c033547cc --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/DefaultClaimUidExtractor.cs @@ -0,0 +1,82 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Security.Cryptography; + +namespace Microsoft.AspNet.Antiforgery +{ + /// + /// Default implementation of . + /// + public class DefaultClaimUidExtractor : IClaimUidExtractor + { + /// + public string ExtractClaimUid(ClaimsIdentity claimsIdentity) + { + if (claimsIdentity == null || !claimsIdentity.IsAuthenticated) + { + // Skip anonymous users + return null; + } + + var uniqueIdentifierParameters = GetUniqueIdentifierParameters(claimsIdentity); + var claimUidBytes = ComputeSHA256(uniqueIdentifierParameters); + return Convert.ToBase64String(claimUidBytes); + } + + // Internal for testing + internal static IEnumerable GetUniqueIdentifierParameters(ClaimsIdentity claimsIdentity) + { + 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 stream = new MemoryStream()) + { + using (var writer = new BinaryWriter(stream)) + { + foreach (string parameter in parameters) + { + writer.Write(parameter); // also writes the length as a prefix; unambiguous + } + + writer.Flush(); + + using (var sha256 = SHA256.Create()) + { + var bytes = sha256.ComputeHash(stream.ToArray(), 0, checked((int)stream.Length)); + return bytes; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Antiforgery/IAntiforgeryAdditionalDataProvider.cs b/src/Microsoft.AspNet.Antiforgery/IAntiforgeryAdditionalDataProvider.cs new file mode 100644 index 0000000000..fb609768bc --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/IAntiforgeryAdditionalDataProvider.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Antiforgery +{ + /// + /// 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.Antiforgery/IAntiforgeryContextAccessor.cs b/src/Microsoft.AspNet.Antiforgery/IAntiforgeryContextAccessor.cs new file mode 100644 index 0000000000..ac2c5e023f --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/IAntiforgeryContextAccessor.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Antiforgery +{ + public interface IAntiforgeryContextAccessor + { + AntiforgeryContext Value { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Antiforgery/IAntiforgeryTokenGenerator.cs b/src/Microsoft.AspNet.Antiforgery/IAntiforgeryTokenGenerator.cs new file mode 100644 index 0000000000..87d936dec3 --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/IAntiforgeryTokenGenerator.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Antiforgery +{ + // Provides configuration information about the anti-forgery system. + public interface IAntiforgeryTokenGenerator + { + // 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.Antiforgery/IAntiforgeryTokenSerializer.cs b/src/Microsoft.AspNet.Antiforgery/IAntiforgeryTokenSerializer.cs new file mode 100644 index 0000000000..4ba30c5591 --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/IAntiforgeryTokenSerializer.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Antiforgery +{ + // Abstracts out the serialization process for an anti-forgery token + public interface IAntiforgeryTokenSerializer + { + AntiforgeryToken Deserialize(string serializedToken); + string Serialize(AntiforgeryToken token); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Antiforgery/IAntiforgeryTokenStore.cs b/src/Microsoft.AspNet.Antiforgery/IAntiforgeryTokenStore.cs new file mode 100644 index 0000000000..0f2ab27024 --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/IAntiforgeryTokenStore.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Antiforgery +{ + // Provides an abstraction around how tokens are persisted and retrieved for a request + public interface IAntiforgeryTokenStore + { + 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.Antiforgery/IAntiforgeryTokenValidator.cs b/src/Microsoft.AspNet.Antiforgery/IAntiforgeryTokenValidator.cs new file mode 100644 index 0000000000..ed75a9de45 --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/IAntiforgeryTokenValidator.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Antiforgery +{ + // Provides an abstraction around something that can validate anti-XSRF tokens + public interface IAntiforgeryTokenValidator + { + // 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.Antiforgery/IClaimUidExtractor.cs b/src/Microsoft.AspNet.Antiforgery/IClaimUidExtractor.cs new file mode 100644 index 0000000000..c7b0af04c9 --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/IClaimUidExtractor.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; + +namespace Microsoft.AspNet.Antiforgery +{ + /// + /// This interface can extract unique identifers for a claims-based identity. + /// + public interface IClaimUidExtractor + { + /// + /// Extracts claims identifier. + /// + /// The . + /// The claims identifier. + string ExtractClaimUid(ClaimsIdentity identity); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Antiforgery/Internal/AntiforgeryWorker.cs b/src/Microsoft.AspNet.Antiforgery/Internal/AntiforgeryWorker.cs new file mode 100644 index 0000000000..ac4dd9e0b8 --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/Internal/AntiforgeryWorker.cs @@ -0,0 +1,251 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.Framework.Internal; +using Microsoft.Framework.WebEncoders; + +namespace Microsoft.AspNet.Antiforgery.Internal +{ + public sealed class AntiforgeryWorker + { + private readonly AntiforgeryOptions _config; + private readonly IAntiforgeryTokenSerializer _serializer; + private readonly IAntiforgeryTokenStore _tokenStore; + private readonly IAntiforgeryTokenValidator _validator; + private readonly IAntiforgeryTokenGenerator _generator; + private readonly IHtmlEncoder _htmlEncoder; + + public AntiforgeryWorker( + [NotNull] IAntiforgeryTokenSerializer serializer, + [NotNull] AntiforgeryOptions config, + [NotNull] IAntiforgeryTokenStore tokenStore, + [NotNull] IAntiforgeryTokenGenerator generator, + [NotNull] IAntiforgeryTokenValidator validator, + [NotNull] IHtmlEncoder htmlEncoder) + { + _serializer = serializer; + _config = config; + _tokenStore = tokenStore; + _generator = generator; + _validator = validator; + _htmlEncoder = htmlEncoder; + } + + private void CheckSSLConfig(HttpContext httpContext) + { + if (_config.RequireSSL && !httpContext.Request.IsHttps) + { + throw new InvalidOperationException(Resources.AntiforgeryWorker_RequireSSL); + } + } + + private AntiforgeryToken DeserializeToken(string serializedToken) + { + return (!string.IsNullOrEmpty(serializedToken)) + ? _serializer.Deserialize(serializedToken) + : null; + } + + private AntiforgeryToken DeserializeTokenDoesNotThrow(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) + { + var user = httpContext.User; + + if (user != null) + { + // We only support ClaimsIdentity. + return user.Identity as ClaimsIdentity; + } + } + + return null; + } + + private AntiforgeryToken GetCookieTokenDoesNotThrow(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 string GetFormInputElement([NotNull] HttpContext httpContext) + { + CheckSSLConfig(httpContext); + + var cookieToken = GetCookieTokenDoesNotThrow(httpContext); + var tokenSet = GetTokens(httpContext, cookieToken); + cookieToken = tokenSet.CookieToken; + var formToken = tokenSet.FormToken; + + SaveCookieTokenAndHeader(httpContext, cookieToken); + + var inputTag = string.Format( + "", + _htmlEncoder.HtmlEncode(_config.FormFieldName), + _htmlEncoder.HtmlEncode("hidden"), + _htmlEncoder.HtmlEncode(_serializer.Serialize(formToken))); + return inputTag; + } + + // [ 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 cookieToken) + { + CheckSSLConfig(httpContext); + var deSerializedcookieToken = DeserializeTokenDoesNotThrow(cookieToken); + var tokenSet = GetTokens(httpContext, deSerializedcookieToken); + + var serializedCookieToken = Serialize(tokenSet.CookieToken); + var serializedFormToken = Serialize(tokenSet.FormToken); + return new AntiforgeryTokenSet(serializedFormToken, serializedCookieToken); + } + + private AntiforgeryTokenSetInternal GetTokens(HttpContext httpContext, AntiforgeryToken cookieToken) + { + var newCookieToken = ValidateAndGenerateNewCookieToken(cookieToken); + if (newCookieToken != null) + { + cookieToken = newCookieToken; + } + var formToken = _generator.GenerateFormToken( + httpContext, + ExtractIdentity(httpContext), + cookieToken); + + return new AntiforgeryTokenSetInternal() + { + // Note : The new cookie would be null if the old cookie is valid. + 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); + } + + + /// + /// Generates and sets an anti-forgery cookie if one is not available or not valid. Also sets response headers. + /// + /// The HTTP context associated with the current call. + public void SetCookieTokenAndHeader([NotNull] HttpContext httpContext) + { + CheckSSLConfig(httpContext); + + var cookieToken = GetCookieTokenDoesNotThrow(httpContext); + cookieToken = ValidateAndGenerateNewCookieToken(cookieToken); + + SaveCookieTokenAndHeader(httpContext, cookieToken); + } + + // This method returns null if oldCookieToken is valid. + private AntiforgeryToken ValidateAndGenerateNewCookieToken(AntiforgeryToken cookieToken) + { + if (!_validator.IsCookieTokenValid(cookieToken)) + { + // Need to make sure we're always operating with a good cookie token. + var newCookieToken = _generator.GenerateCookieToken(); + Debug.Assert(_validator.IsCookieTokenValid(newCookieToken)); + return newCookieToken; + } + + return null; + } + + private void SaveCookieTokenAndHeader( + [NotNull] HttpContext httpContext, + AntiforgeryToken cookieToken) + { + if (cookieToken != null) + { + // Persist the new cookie if it is not null. + _tokenStore.SaveCookieToken(httpContext, cookieToken); + } + + 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.Set("X-Frame-Options", "SAMEORIGIN"); + } + } + + 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.Antiforgery/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Antiforgery/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..a91599ff84 --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/Properties/AssemblyInfo.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +[assembly: InternalsVisibleTo("Microsoft.AspNet.Antiforgery.Test")] +[assembly: AssemblyMetadata("Serviceable", "True")] diff --git a/src/Microsoft.AspNet.Antiforgery/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Antiforgery/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..af584b07c3 --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/Properties/Resources.Designer.cs @@ -0,0 +1,206 @@ +// +namespace Microsoft.AspNet.Antiforgery +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNet.Antiforgery.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 AntiforgeryTokenValidator_AuthenticatedUserWithoutUsername + { + get { return GetString("AntiforgeryTokenValidator_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 FormatAntiforgeryTokenValidator_AuthenticatedUserWithoutUsername(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("AntiforgeryTokenValidator_AuthenticatedUserWithoutUsername"), p0); + } + + /// + /// 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. + /// + internal static string AntiforgeryToken_DeserializationFailed + { + get { return GetString("AntiforgeryToken_DeserializationFailed"); } + } + + /// + /// The anti-forgery token could not be decrypted. + /// + 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 AntiforgeryOptions.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 AntiforgeryOptions.RequireSsl = true, but the current request is not an SSL request. + /// + internal static string FormatAntiforgeryWorker_RequireSSL() + { + return GetString("AntiforgeryWorker_RequireSSL"); + } + + /// + /// Value cannot be null or empty. + /// + internal static string ArgumentCannotBeNullOrEmpty + { + get { return GetString("ArgumentCannotBeNullOrEmpty"); } + } + + /// + /// Value cannot be null or empty. + /// + internal static string FormatArgumentCannotBeNullOrEmpty() + { + return GetString("ArgumentCannotBeNullOrEmpty"); + } + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Microsoft.AspNet.Antiforgery/Resources.resx b/src/Microsoft.AspNet.Antiforgery/Resources.resx new file mode 100644 index 0000000000..e666385354 --- /dev/null +++ b/src/Microsoft.AspNet.Antiforgery/Resources.resx @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 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. + + + 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 AntiforgeryOptions.RequireSsl = true, but the current request is not an SSL request. + + + Value cannot be null or empty. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Antiforgery/project.json b/src/Microsoft.AspNet.Antiforgery/project.json index 9840dfddec..10cb97d9a1 100644 --- a/src/Microsoft.AspNet.Antiforgery/project.json +++ b/src/Microsoft.AspNet.Antiforgery/project.json @@ -1,22 +1,30 @@ { - "version": "1.0.0-*", - "description": "", - "authors": [ "" ], - "tags": [ "" ], - "projectUrl": "", - "licenseUrl": "", + "version": "1.0.0-*", + "description": "", + "authors": [ "" ], + "tags": [ "" ], + "projectUrl": "", + "licenseUrl": "", - "dependencies": { - }, + "dependencies": { + "Microsoft.AspNet.DataProtection": "1.0.0-*", + "Microsoft.AspNet.Http.Abstractions": "1.0.0-*", + "Microsoft.AspNet.WebUtilities": "1.0.0-*", + "Microsoft.Framework.DependencyInjection.Abstractions": "1.0.0-*", + "Microsoft.Framework.NotNullAttribute.Sources": { "type": "build", "version": "1.0.0-*" }, + "Microsoft.Framework.OptionsModel": "1.0.0-*" + }, "frameworks" : { "dnx451": { }, "dnxcore50" : { "dependencies": { - "System.Collections": "4.0.10-beta-22807", - "System.Linq": "4.0.0-beta-22807", - "System.Threading": "4.0.10-beta-22807", - "Microsoft.CSharp": "4.0.0-beta-22807" + "System.Collections": "4.0.10-beta-*", + "System.Linq": "4.0.0-beta-*", + "System.Security.Cryptography.RandomNumberGenerator": "4.0.0-beta-*", + "System.Security.Cryptography.Hashing.Algorithms": "4.0.0-beta-*", + "System.Threading": "4.0.10-beta-*", + "Microsoft.CSharp": "4.0.0-beta-*" } } } diff --git a/test/Microsoft.AspNet.Antiforgery.Test/AntiforgeryTokenSerializerTest.cs b/test/Microsoft.AspNet.Antiforgery.Test/AntiforgeryTokenSerializerTest.cs new file mode 100644 index 0000000000..dae7de6a17 --- /dev/null +++ b/test/Microsoft.AspNet.Antiforgery.Test/AntiforgeryTokenSerializerTest.cs @@ -0,0 +1,184 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if DNX451 +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.DataProtection; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Antiforgery +{ + public class AntiforgeryTokenSerializerTest + { + private static readonly Mock _dataProtector = GetDataProtector(); + 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 }); + private const byte _salt = 0x05; + + [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_Throws(string serializedToken) + { + // Arrange + var testSerializer = new AntiforgeryTokenSerializer(_dataProtector.Object); + + // Act & assert + var ex = Assert.Throws(() => testSerializer.Deserialize(serializedToken)); + Assert.Equal(@"The anti-forgery token could not be decrypted.", ex.Message); + } + + [Fact] + public void Serialize_FieldToken_WithClaimUid_TokenRoundTripSuccessful() + { + // Arrange + var testSerializer = new AntiforgeryTokenSerializer(_dataProtector.Object); + + //"01" // Version + //+ "705EEDCC7D42F1D6B3B98A593625BB4C" // SecurityToken + //+ "00" // IsSessionToken + //+ "01" // IsClaimsBased + //+ "6F1648E97249AA58754036A67E248CF044F07ECFB0ED387556CE029A4F9A40E0" // ClaimUid + //+ "05" // AdditionalData length header + //+ "E282AC3437"; // AdditionalData ("€47") as UTF8 + var token = new AntiforgeryToken() + { + SecurityToken = _securityToken, + IsSessionToken = false, + ClaimUid = _claimUid, + AdditionalData = "€47" + }; + + // Act + var actualSerializedData = testSerializer.Serialize(token); + var deserializedToken = testSerializer.Deserialize(actualSerializedData); + + // Assert + AssertTokensEqual(token, deserializedToken); + _dataProtector.Verify(); + } + + [Fact] + public void Serialize_FieldToken_WithUsername_TokenRoundTripSuccessful() + { + // Arrange + var testSerializer = new AntiforgeryTokenSerializer(_dataProtector.Object); + + //"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 + var token = new AntiforgeryToken() + { + SecurityToken = _securityToken, + IsSessionToken = false, + Username = "Jérôme", + AdditionalData = "€47" + }; + + // Act + var actualSerializedData = testSerializer.Serialize(token); + var deserializedToken = testSerializer.Deserialize(actualSerializedData); + + // Assert + AssertTokensEqual(token, deserializedToken); + _dataProtector.Verify(); + } + + [Fact] + public void Serialize_SessionToken_TokenRoundTripSuccessful() + { + // Arrange + var testSerializer = new AntiforgeryTokenSerializer(_dataProtector.Object); + + //"01" // Version + //+ "705EEDCC7D42F1D6B3B98A593625BB4C" // SecurityToken + //+ "01"; // IsSessionToken + var token = new AntiforgeryToken() + { + SecurityToken = _securityToken, + IsSessionToken = true + }; + + // Act + string actualSerializedData = testSerializer.Serialize(token); + var deserializedToken = testSerializer.Deserialize(actualSerializedData); + + // Assert + AssertTokensEqual(token, deserializedToken); + _dataProtector.Verify(); + } + + private static Mock GetDataProtector() + { + var mockCryptoSystem = new Mock(); + mockCryptoSystem.Setup(o => o.Protect(It.IsAny())) + .Returns(Protect) + .Verifiable(); + mockCryptoSystem.Setup(o => o.Unprotect(It.IsAny())) + .Returns(UnProtect) + .Verifiable(); + return mockCryptoSystem; + } + + private static byte[] Protect(byte[] data) + { + var input = new List(data); + input.Add(_salt); + return input.ToArray(); + } + + private static byte[] UnProtect(byte[] data) + { + var salt = data[data.Length - 1]; + if (salt != _salt) + { + throw new ArgumentException("Invalid salt value in data"); + } + + return data.Take(data.Length - 1).ToArray(); + } + + 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); + } + } +} + +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Antiforgery.Test/AntiforgeryTokenStoreTest.cs b/test/Microsoft.AspNet.Antiforgery.Test/AntiforgeryTokenStoreTest.cs new file mode 100644 index 0000000000..b8e45bc0e1 --- /dev/null +++ b/test/Microsoft.AspNet.Antiforgery.Test/AntiforgeryTokenStoreTest.cs @@ -0,0 +1,428 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if DNX451 +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.Framework.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Antiforgery +{ + public class AntiforgeryTokenStoreTest + { + private readonly string _cookieName = "cookie-name"; + + [Fact] + public void GetCookieToken_CookieDoesNotExist_ReturnsNull() + { + // Arrange + var requestCookies = new Mock(); + requestCookies + .Setup(o => o.Get(It.IsAny())) + .Returns(string.Empty); + var mockHttpContext = new Mock(); + mockHttpContext + .Setup(o => o.Request.Cookies) + .Returns(requestCookies.Object); + var contextAccessor = new AntiforgeryContextAccessor(); + mockHttpContext.SetupGet(o => o.RequestServices) + .Returns(GetServiceProvider(contextAccessor)); + var config = new AntiforgeryOptions() + { + CookieName = _cookieName + }; + + var tokenStore = new AntiforgeryTokenStore( + config: config, + serializer: null); + + // Act + var token = tokenStore.GetCookieToken(mockHttpContext.Object); + + // Assert + Assert.Null(token); + } + + [Fact] + public void GetCookieToken_CookieIsMissingInRequest_LooksUpCookieInAntiforgeryContext() + { + // Arrange + var requestCookies = new Mock(); + requestCookies + .Setup(o => o.Get(It.IsAny())) + .Returns(string.Empty); + var mockHttpContext = new Mock(); + mockHttpContext + .Setup(o => o.Request.Cookies) + .Returns(requestCookies.Object); + var contextAccessor = new AntiforgeryContextAccessor(); + mockHttpContext.SetupGet(o => o.RequestServices) + .Returns(GetServiceProvider(contextAccessor)); + + // add a cookie explicitly. + var cookie = new AntiforgeryToken(); + contextAccessor.Value = new AntiforgeryContext() { CookieToken = cookie }; + var config = new AntiforgeryOptions() + { + CookieName = _cookieName + }; + + var tokenStore = new AntiforgeryTokenStore( + config: config, + serializer: null); + + // Act + var token = tokenStore.GetCookieToken(mockHttpContext.Object); + + // Assert + Assert.Equal(cookie, token); + } + + [Fact] + public void GetCookieToken_CookieIsEmpty_ReturnsNull() + { + // Arrange + var mockHttpContext = GetMockHttpContext(_cookieName, string.Empty); + + var config = new AntiforgeryOptions() + { + CookieName = _cookieName + }; + + var tokenStore = new AntiforgeryTokenStore( + config: config, + serializer: null); + + // Act + var token = tokenStore.GetCookieToken(mockHttpContext); + + // Assert + Assert.Null(token); + } + + [Fact] + public void GetCookieToken_CookieIsInvalid_PropagatesException() + { + // Arrange + var mockHttpContext = GetMockHttpContext(_cookieName, "invalid-value"); + var config = new AntiforgeryOptions() + { + CookieName = _cookieName + }; + + var expectedException = new InvalidOperationException("some exception"); + var mockSerializer = new Mock(); + mockSerializer + .Setup(o => o.Deserialize("invalid-value")) + .Throws(expectedException); + + var tokenStore = new AntiforgeryTokenStore( + config: config, + serializer: mockSerializer.Object); + + // Act & assert + var ex = Assert.Throws(() => tokenStore.GetCookieToken(mockHttpContext)); + Assert.Same(expectedException, ex); + } + + [Fact] + public void GetCookieToken_CookieIsValid_ReturnsToken() + { + // Arrange + var expectedToken = new AntiforgeryToken(); + var mockHttpContext = GetMockHttpContext(_cookieName, "valid-value"); + + var config = new AntiforgeryOptions() + { + CookieName = _cookieName + }; + + var mockSerializer = new Mock(); + mockSerializer + .Setup(o => o.Deserialize("valid-value")) + .Returns(expectedToken); + + var tokenStore = new AntiforgeryTokenStore( + config: config, + serializer: mockSerializer.Object); + + // Act + AntiforgeryToken retVal = tokenStore.GetCookieToken(mockHttpContext); + + // Assert + Assert.Same(expectedToken, retVal); + } + + [Fact] + public async Task GetFormToken_FormFieldIsEmpty_ReturnsNull() + { + // Arrange + var mockHttpContext = new Mock(); + var requestContext = new Mock(); + var formCollection = new Mock(); + formCollection.Setup(f => f["form-field-name"]).Returns(string.Empty); + requestContext.Setup(o => o.ReadFormAsync(CancellationToken.None)) + .Returns(Task.FromResult(formCollection.Object)); + mockHttpContext.Setup(o => o.Request) + .Returns(requestContext.Object); + var config = new AntiforgeryOptions() + { + FormFieldName = "form-field-name" + }; + + var tokenStore = new AntiforgeryTokenStore( + config: config, + serializer: null); + + // Act + var token = await tokenStore.GetFormTokenAsync(mockHttpContext.Object); + + // Assert + Assert.Null(token); + } + + [Fact] + public async Task GetFormToken_FormFieldIsInvalid_PropagatesException() + { + // Arrange + var formCollection = new Mock(); + formCollection.Setup(f => f["form-field-name"]).Returns("invalid-value"); + + var requestContext = new Mock(); + requestContext.Setup(o => o.ReadFormAsync(CancellationToken.None)) + .Returns(Task.FromResult(formCollection.Object)); + + var mockHttpContext = new Mock(); + mockHttpContext.Setup(o => o.Request) + .Returns(requestContext.Object); + + var config = new AntiforgeryOptions() + { + FormFieldName = "form-field-name" + }; + + var expectedException = new InvalidOperationException("some exception"); + var mockSerializer = new Mock(); + mockSerializer.Setup(o => o.Deserialize("invalid-value")) + .Throws(expectedException); + + var tokenStore = new AntiforgeryTokenStore( + config: config, + serializer: mockSerializer.Object); + + // Act & assert + var ex = + await + Assert.ThrowsAsync( + async () => await tokenStore.GetFormTokenAsync(mockHttpContext.Object)); + Assert.Same(expectedException, ex); + } + + [Fact] + public async Task GetFormToken_FormFieldIsValid_ReturnsToken() + { + // Arrange + var expectedToken = new AntiforgeryToken(); + + // Arrange + var mockHttpContext = new Mock(); + var requestContext = new Mock(); + var formCollection = new Mock(); + formCollection.Setup(f => f["form-field-name"]).Returns("valid-value"); + requestContext.Setup(o => o.ReadFormAsync(CancellationToken.None)) + .Returns(Task.FromResult(formCollection.Object)); + mockHttpContext.Setup(o => o.Request) + .Returns(requestContext.Object); + + var config = new AntiforgeryOptions() + { + FormFieldName = "form-field-name" + }; + + var mockSerializer = new Mock(); + mockSerializer.Setup(o => o.Deserialize("valid-value")) + .Returns(expectedToken); + + var tokenStore = new AntiforgeryTokenStore( + config: config, + serializer: mockSerializer.Object); + + // Act + var retVal = await tokenStore.GetFormTokenAsync(mockHttpContext.Object); + + // Assert + Assert.Same(expectedToken, retVal); + } + + [Theory] + [InlineData(true, true)] + [InlineData(false, null)] + public void SaveCookieToken(bool requireSsl, bool? expectedCookieSecureFlag) + { + // Arrange + var token = new AntiforgeryToken(); + var mockCookies = new Mock(); + + bool defaultCookieSecureValue = expectedCookieSecureFlag ?? false; // pulled from config; set by ctor + var cookies = new MockResponseCookieCollection(); + + cookies.Count = 0; + var mockHttpContext = new Mock(); + mockHttpContext.Setup(o => o.Response.Cookies) + .Returns(cookies); + var contextAccessor = new AntiforgeryContextAccessor(); + mockHttpContext.SetupGet(o => o.RequestServices) + .Returns(GetServiceProvider(contextAccessor)); + + var mockSerializer = new Mock(); + mockSerializer.Setup(o => o.Serialize(token)) + .Returns("serialized-value"); + + var config = new AntiforgeryOptions() + { + CookieName = _cookieName, + RequireSSL = requireSsl + }; + + var tokenStore = new AntiforgeryTokenStore( + config: config, + serializer: mockSerializer.Object); + + // Act + tokenStore.SaveCookieToken(mockHttpContext.Object, token); + + // Assert + Assert.Equal(1, cookies.Count); + Assert.NotNull(contextAccessor.Value.CookieToken); + Assert.NotNull(cookies); + Assert.Equal(_cookieName, cookies.Key); + Assert.Equal("serialized-value", cookies.Value); + Assert.True(cookies.Options.HttpOnly); + Assert.Equal(defaultCookieSecureValue, cookies.Options.Secure); + } + + private HttpContext GetMockHttpContext(string cookieName, string cookieValue) + { + var requestCookies = new MockCookieCollection(new Dictionary() { { cookieName, cookieValue } }); + + var request = new Mock(); + request.Setup(o => o.Cookies) + .Returns(requestCookies); + var mockHttpContext = new Mock(); + mockHttpContext.Setup(o => o.Request) + .Returns(request.Object); + + var contextAccessor = new AntiforgeryContextAccessor(); + mockHttpContext.SetupGet(o => o.RequestServices) + .Returns(GetServiceProvider(contextAccessor)); + + return mockHttpContext.Object; + } + + private static IServiceProvider GetServiceProvider(IAntiforgeryContextAccessor contextAccessor) + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddInstance(contextAccessor); + return serviceCollection.BuildServiceProvider(); + } + + private class MockResponseCookieCollection : IResponseCookies + { + public string Key { get; set; } + public string Value { get; set; } + public CookieOptions Options { get; set; } + public int Count { get; set; } + + public void Append(string key, string value, CookieOptions options) + { + this.Key = key; + this.Value = value; + this.Options = options; + this.Count++; + } + + public void Append(string key, string value) + { + throw new NotImplementedException(); + } + + public void Delete(string key, CookieOptions options) + { + throw new NotImplementedException(); + } + + public void Delete(string key) + { + throw new NotImplementedException(); + } + } + + private class MockCookieCollection : IReadableStringCollection + { + private Dictionary _dictionary; + + public int Count + { + get + { + return _dictionary.Count; + } + } + + public ICollection Keys + { + get + { + return _dictionary.Keys; + } + } + + public MockCookieCollection(Dictionary dictionary) + { + _dictionary = dictionary; + } + + public static MockCookieCollection GetDummyInstance(string key, string value) + { + return new MockCookieCollection(new Dictionary() { { key, value } }); + } + + public string Get(string key) + { + return this[key]; + } + + public IList GetValues(string key) + { + throw new NotImplementedException(); + } + + public bool ContainsKey(string key) + { + return _dictionary.ContainsKey(key); + } + + public string this[string key] + { + get { return _dictionary[key]; } + } + + public IEnumerator> GetEnumerator() + { + throw new NotImplementedException(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + } + } +} + +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Antiforgery.Test/AntiforgeryTokenTest.cs b/test/Microsoft.AspNet.Antiforgery.Test/AntiforgeryTokenTest.cs new file mode 100644 index 0000000000..cddd47e43f --- /dev/null +++ b/test/Microsoft.AspNet.Antiforgery.Test/AntiforgeryTokenTest.cs @@ -0,0 +1,132 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.AspNet.Antiforgery +{ + public class AntiforgeryTokenTest + { + [Fact] + public void AdditionalDataProperty() + { + // Arrange + var 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 + var 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 + var 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 UsernameProperty() + { + // Arrange + var 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); + } + + [Fact] + public void SecurityTokenProperty_GetsAutopopulated() + { + // Arrange + var token = new AntiforgeryToken(); + + // Act + var securityToken = token.SecurityToken; + + // Assert + Assert.NotNull(securityToken); + Assert.Equal(AntiforgeryToken.SecurityTokenBitLength, securityToken.BitLength); + + // check that we're not making a new one each property call + Assert.Equal(securityToken, token.SecurityToken); + } + + [Fact] + public void SecurityTokenProperty_PropertySetter_DoesNotUseDefaults() + { + // Arrange + var token = new AntiforgeryToken(); + + // Act + var securityToken = new BinaryBlob(64); + token.SecurityToken = securityToken; + + // Assert + Assert.Equal(securityToken, token.SecurityToken); + } + + [Fact] + public void SecurityTokenProperty_PropertySetter_DoesNotAllowNulls() + { + // Arrange + var token = new AntiforgeryToken(); + + // Act + token.SecurityToken = null; + var securityToken = token.SecurityToken; + + // Assert + Assert.NotNull(securityToken); + Assert.Equal(AntiforgeryToken.SecurityTokenBitLength, securityToken.BitLength); + + // check that we're not making a new one each property call + Assert.Equal(securityToken, token.SecurityToken); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Antiforgery.Test/AntiforgeryWorkerTest.cs b/test/Microsoft.AspNet.Antiforgery.Test/AntiforgeryWorkerTest.cs new file mode 100644 index 0000000000..899a1f0225 --- /dev/null +++ b/test/Microsoft.AspNet.Antiforgery.Test/AntiforgeryWorkerTest.cs @@ -0,0 +1,583 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if DNX451 + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Antiforgery.Internal; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Internal; +using Microsoft.Framework.WebEncoders.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Antiforgery +{ + public class AntiforgeryWorkerTest + { + + [Fact] + public async Task ChecksSSL_ValidateAsync_Throws() + { + // Arrange + var mockHttpContext = new Mock(); + mockHttpContext.Setup(o => o.Request.IsHttps) + .Returns(false); + + var config = new AntiforgeryOptions() + { + RequireSSL = true + }; + + var worker = new AntiforgeryWorker( + config: config, + serializer: null, + tokenStore: null, + generator: null, + validator: null, + htmlEncoder: new CommonTestEncoder()); + + // Act & assert + var ex = + await + Assert.ThrowsAsync( + async () => await worker.ValidateAsync(mockHttpContext.Object)); + Assert.Equal( + @"The anti-forgery system has the configuration value AntiforgeryOptions.RequireSsl = true, " + + "but the current request is not an SSL request.", + ex.Message); + } + + [Fact] + public void ChecksSSL_Validate_Throws() + { + // Arrange + var mockHttpContext = new Mock(); + mockHttpContext.Setup(o => o.Request.IsHttps) + .Returns(false); + + var config = new AntiforgeryOptions() + { + RequireSSL = true + }; + + var worker = new AntiforgeryWorker( + config: config, + serializer: null, + tokenStore: null, + generator: null, + validator: null, + htmlEncoder: new CommonTestEncoder()); + + // Act & assert + var ex = Assert.Throws( + () => worker.Validate(mockHttpContext.Object, cookieToken: null, formToken: null)); + Assert.Equal( + @"The anti-forgery system has the configuration value AntiforgeryOptions.RequireSsl = true, " + + "but the current request is not an SSL request.", + ex.Message); + } + + [Fact] + public void ChecksSSL_GetFormInputElement_Throws() + { + // Arrange + var mockHttpContext = new Mock(); + mockHttpContext.Setup(o => o.Request.IsHttps) + .Returns(false); + + var config = new AntiforgeryOptions() + { + RequireSSL = true + }; + + var worker = new AntiforgeryWorker( + config: config, + serializer: null, + tokenStore: null, + generator: null, + validator: null, + htmlEncoder: new CommonTestEncoder()); + + // Act & assert + var ex = Assert.Throws(() => worker.GetFormInputElement(mockHttpContext.Object)); + Assert.Equal( + @"The anti-forgery system has the configuration value AntiforgeryOptions.RequireSsl = true, " + + "but the current request is not an SSL request.", + ex.Message); + } + + [Fact] + public void ChecksSSL_GetTokens_Throws() + { + // Arrange + var mockHttpContext = new Mock(); + mockHttpContext.Setup(o => o.Request.IsHttps) + .Returns(false); + + var config = new AntiforgeryOptions() + { + RequireSSL = true + }; + + var worker = new AntiforgeryWorker( + config: config, + serializer: null, + tokenStore: null, + generator: null, + validator: null, + htmlEncoder: new CommonTestEncoder()); + + // Act & assert + var ex = Assert.Throws(() => + worker.GetTokens(mockHttpContext.Object, "cookie-token")); + Assert.Equal( + @"The anti-forgery system has the configuration value AntiforgeryOptions.RequireSsl = true, " + + "but the current request is not an SSL request.", + ex.Message); + } + + [Fact] + public void GetFormInputElement_ExistingInvalidCookieToken_GeneratesANewCookieAndAnAntiforgeryToken() + { + // Arrange + var config = new AntiforgeryOptions() + { + FormFieldName = "form-field-name" + }; + + // Make sure the existing cookie is invalid. + var context = GetAntiforgeryWorkerContext(config, isOldCookieValid: false); + var worker = GetAntiforgeryWorker(context); + + // Act + var inputElement = worker.GetFormInputElement(context.HttpContext.Object); + + // Assert + Assert.Equal( + @"", + inputElement); + context.TokenStore.Verify(); + } + + [Fact] + public void GetFormInputElement_ExistingInvalidCookieToken_SwallowsExceptions() + { + // Arrange + var config = new AntiforgeryOptions() + { + FormFieldName = "form-field-name" + }; + + // Make sure the existing cookie is invalid. + var context = GetAntiforgeryWorkerContext(config, isOldCookieValid: false); + var worker = GetAntiforgeryWorker(context); + + // This will cause the cookieToken to be null. + context.TokenStore.Setup(o => o.GetCookieToken(context.HttpContext.Object)) + .Throws(new Exception("should be swallowed")); + + // Setup so that the null cookie token returned is treated as invalid. + context.TokenValidator.Setup(o => o.IsCookieTokenValid(null)) + .Returns(false); + + // Act + var inputElement = worker.GetFormInputElement(context.HttpContext.Object); + + // Assert + Assert.Equal( + @"", + inputElement); + context.TokenStore.Verify(); + } + + [Fact] + public void GetFormInputElement_ExistingValidCookieToken_GeneratesAnAntiforgeryToken() + { + // Arrange + var options = new AntiforgeryOptions() + { + FormFieldName = "form-field-name" + }; + + // Make sure the existing cookie is valid and use the same cookie for the mock Token Provider. + var context = GetAntiforgeryWorkerContext(options, useOldCookie: true, isOldCookieValid: true); + var worker = GetAntiforgeryWorker(context); + + // Act + var inputElement = worker.GetFormInputElement(context.HttpContext.Object); + + // Assert + Assert.Equal( + @"", + inputElement); + } + + [Theory] + [InlineData(false, "SAMEORIGIN")] + [InlineData(true, null)] + public void GetFormInputElement_AddsXFrameOptionsHeader(bool suppressXFrameOptions, string expectedHeaderValue) + { + // Arrange + var options = new AntiforgeryOptions() + { + SuppressXFrameOptionsHeader = suppressXFrameOptions + }; + + // Genreate a new cookie. + var context = GetAntiforgeryWorkerContext(options, useOldCookie: false, isOldCookieValid: false); + var worker = GetAntiforgeryWorker(context); + + // Act + var inputElement = worker.GetFormInputElement(context.HttpContext.Object); + + // Assert + string xFrameOptions = context.HttpContext.Object.Response.Headers["X-Frame-Options"]; + Assert.Equal(expectedHeaderValue, xFrameOptions); + } + + [Fact] + public void GetTokens_ExistingInvalidCookieToken_GeneratesANewCookieTokenAndANewFormToken() + { + // Arrange + // Genreate a new cookie. + var context = GetAntiforgeryWorkerContext( + new AntiforgeryOptions(), + useOldCookie: false, + isOldCookieValid: false); + var worker = GetAntiforgeryWorker(context); + + // Act + var tokenset = worker.GetTokens(context.HttpContext.Object, "serialized-old-cookie-token"); + + // Assert + Assert.Equal("serialized-new-cookie-token", tokenset.CookieToken); + Assert.Equal("serialized-form-token", tokenset.FormToken); + } + + [Fact] + public void GetTokens_ExistingInvalidCookieToken_SwallowsExceptions() + { + // Arrange + // Make sure the existing cookie is invalid. + var context = GetAntiforgeryWorkerContext( + new AntiforgeryOptions(), + useOldCookie: false, + isOldCookieValid: false); + + // This will cause the cookieToken to be null. + context.TokenSerializer.Setup(o => o.Deserialize("serialized-old-cookie-token")) + .Throws(new Exception("should be swallowed")); + + // Setup so that the null cookie token returned is treated as invalid. + context.TokenValidator.Setup(o => o.IsCookieTokenValid(null)) + .Returns(false); + var worker = GetAntiforgeryWorker(context); + + // Act + var tokenset = worker.GetTokens(context.HttpContext.Object, "serialized-old-cookie-token"); + + // Assert + Assert.Equal("serialized-new-cookie-token", tokenset.CookieToken); + Assert.Equal("serialized-form-token", tokenset.FormToken); + } + + [Fact] + public void GetTokens_ExistingValidCookieToken_GeneratesANewFormToken() + { + // Arrange + var context = GetAntiforgeryWorkerContext( + new AntiforgeryOptions(), + useOldCookie: true, + isOldCookieValid: true); + context.TokenStore = null; + var worker = GetAntiforgeryWorker(context); + + // Act + var tokenset = worker.GetTokens(context.HttpContext.Object, "serialized-old-cookie-token"); + + // Assert + Assert.Null(tokenset.CookieToken); + Assert.Equal("serialized-form-token", tokenset.FormToken); + } + + [Fact] + public void Validate_FromInvalidStrings_Throws() + { + // Arrange + var context = GetAntiforgeryWorkerContext(new AntiforgeryOptions()); + + context.TokenSerializer.Setup(o => o.Deserialize("cookie-token")) + .Returns(context.TestTokenSet.OldCookieToken); + context.TokenSerializer.Setup(o => o.Deserialize("form-token")) + .Returns(context.TestTokenSet.FormToken); + + context.TokenValidator.Setup(o => o.ValidateTokens( + context.HttpContext.Object, + context.HttpContext.Object.User.Identity as ClaimsIdentity, + context.TestTokenSet.OldCookieToken, context.TestTokenSet.FormToken)) + .Throws(new InvalidOperationException("my-message")); + context.TokenStore = null; + var worker = GetAntiforgeryWorker(context); + + // Act & assert + var ex = + Assert.Throws( + () => worker.Validate(context.HttpContext.Object, "cookie-token", "form-token")); + Assert.Equal("my-message", ex.Message); + } + + [Fact] + public void Validate_FromValidStrings_TokensValidatedSuccessfully() + { + // Arrange + var context = GetAntiforgeryWorkerContext(new AntiforgeryOptions()); + + context.TokenSerializer.Setup(o => o.Deserialize("cookie-token")) + .Returns(context.TestTokenSet.OldCookieToken); + context.TokenSerializer.Setup(o => o.Deserialize("form-token")) + .Returns(context.TestTokenSet.FormToken); + + context.TokenValidator.Setup(o => o.ValidateTokens( + context.HttpContext.Object, + context.HttpContext.Object.User.Identity as ClaimsIdentity, + context.TestTokenSet.OldCookieToken, context.TestTokenSet.FormToken)) + .Verifiable(); + context.TokenStore = null; + var worker = GetAntiforgeryWorker(context); + + // Act + worker.Validate(context.HttpContext.Object, "cookie-token", "form-token"); + + // Assert + context.TokenValidator.Verify(); + } + + [Fact] + public async Task Validate_FromStore_Failure() + { + // Arrange + var context = GetAntiforgeryWorkerContext(new AntiforgeryOptions()); + + context.TokenValidator.Setup(o => o.ValidateTokens( + context.HttpContext.Object, + context.HttpContext.Object.User.Identity as ClaimsIdentity, + context.TestTokenSet.OldCookieToken, context.TestTokenSet.FormToken)) + .Throws(new InvalidOperationException("my-message")); + context.TokenSerializer = null; + var worker = GetAntiforgeryWorker(context); + + // Act & assert + var ex = + await + Assert.ThrowsAsync( + async () => await worker.ValidateAsync(context.HttpContext.Object)); + Assert.Equal("my-message", ex.Message); + } + + [Fact] + public async Task Validate_FromStore_Success() + { + // Arrange + var context = GetAntiforgeryWorkerContext(new AntiforgeryOptions()); + + context.TokenValidator.Setup(o => o.ValidateTokens( + context.HttpContext.Object, + context.HttpContext.Object.User.Identity as ClaimsIdentity, + context.TestTokenSet.OldCookieToken, context.TestTokenSet.FormToken)) + .Verifiable(); + context.TokenSerializer = null; + var worker = GetAntiforgeryWorker(context); + + // Act + await worker.ValidateAsync(context.HttpContext.Object); + + // Assert + context.TokenValidator.Verify(); + } + + [Theory] + [InlineData(false, "SAMEORIGIN")] + [InlineData(true, null)] + public void SetCookieTokenAndHeader_AddsXFrameOptionsHeader( + bool suppressXFrameOptions, + string expectedHeaderValue) + { + // Arrange + var options = new AntiforgeryOptions() + { + SuppressXFrameOptionsHeader = suppressXFrameOptions + }; + + // Genreate a new cookie. + var context = GetAntiforgeryWorkerContext(options, useOldCookie: false, isOldCookieValid: false); + var worker = GetAntiforgeryWorker(context); + + // Act + worker.SetCookieTokenAndHeader(context.HttpContext.Object); + + // Assert + var xFrameOptions = context.HttpContext.Object.Response.Headers["X-Frame-Options"]; + Assert.Equal(expectedHeaderValue, xFrameOptions); + } + + private AntiforgeryWorker GetAntiforgeryWorker(AntiforgeryWorkerContext context) + { + return new AntiforgeryWorker( + config: context.Options, + serializer: context.TokenSerializer != null ? context.TokenSerializer.Object : null, + tokenStore: context.TokenStore != null ? context.TokenStore.Object : null, + generator: context.TokenGenerator != null ? context.TokenGenerator.Object : null, + validator: context.TokenValidator != null ? context.TokenValidator.Object : null, + htmlEncoder: new CommonTestEncoder()); + } + + private Mock GetHttpContext(bool setupResponse = true) + { + var identity = new ClaimsIdentity("some-auth"); + var mockHttpContext = new Mock(); + mockHttpContext.Setup(o => o.User) + .Returns(new ClaimsPrincipal(identity)); + + if (setupResponse) + { + var mockResponse = new Mock(); + mockResponse.Setup(r => r.Headers) + .Returns(new HeaderDictionary(new Dictionary())); + mockHttpContext.Setup(o => o.Response) + .Returns(mockResponse.Object); + } + + return mockHttpContext; + } + + private Mock GetTokenStore( + HttpContext context, + TestTokenSet testTokenSet, + bool saveNewCookie = true) + { + var oldCookieToken = testTokenSet.OldCookieToken; + var formToken = testTokenSet.FormToken; + var mockTokenStore = new Mock(MockBehavior.Strict); + mockTokenStore.Setup(o => o.GetCookieToken(context)) + .Returns(oldCookieToken); + mockTokenStore.Setup(o => o.GetFormTokenAsync(context)) + .Returns(Task.FromResult(formToken)); + + if (saveNewCookie) + { + var newCookieToken = testTokenSet.NewCookieToken; + mockTokenStore.Setup(o => o.SaveCookieToken(context, newCookieToken)) + .Verifiable(); + } + + return mockTokenStore; + } + + private Mock GetTokenSerializer(TestTokenSet testTokenSet) + { + var oldCookieToken = testTokenSet.OldCookieToken; + var newCookieToken = testTokenSet.NewCookieToken; + var formToken = testTokenSet.FormToken; + var mockSerializer = new Mock(MockBehavior.Strict); + mockSerializer.Setup(o => o.Serialize(formToken)) + .Returns("serialized-form-token"); + mockSerializer.Setup(o => o.Deserialize("serialized-old-cookie-token")) + .Returns(oldCookieToken); + mockSerializer.Setup(o => o.Serialize(newCookieToken)) + .Returns("serialized-new-cookie-token"); + return mockSerializer; + } + + private TestTokenSet GetTokenSet(bool isOldCookieTokenSessionToken = true, bool isNewCookieSessionToken = true) + { + return new TestTokenSet() + { + FormToken = new AntiforgeryToken() { IsSessionToken = false }, + OldCookieToken = new AntiforgeryToken() { IsSessionToken = isOldCookieTokenSessionToken }, + NewCookieToken = new AntiforgeryToken() { IsSessionToken = isNewCookieSessionToken }, + }; + } + + private AntiforgeryWorkerContext GetAntiforgeryWorkerContext( + AntiforgeryOptions config, + bool useOldCookie = false, + bool isOldCookieValid = true) + { + // Arrange + var mockHttpContext = GetHttpContext(); + var testTokenSet = GetTokenSet(isOldCookieTokenSessionToken: true, isNewCookieSessionToken: true); + + var mockSerializer = GetTokenSerializer(testTokenSet); + + var mockTokenStore = GetTokenStore(mockHttpContext.Object, testTokenSet); + + var mockGenerator = new Mock(MockBehavior.Strict); + mockGenerator + .Setup(o => o.GenerateFormToken( + mockHttpContext.Object, + mockHttpContext.Object.User.Identity as ClaimsIdentity, + useOldCookie ? testTokenSet.OldCookieToken : testTokenSet.NewCookieToken)) + .Returns(testTokenSet.FormToken); + + mockGenerator + .Setup(o => o.GenerateCookieToken()) + .Returns(useOldCookie ? testTokenSet.OldCookieToken : testTokenSet.NewCookieToken); + + var mockValidator = new Mock(MockBehavior.Strict); + mockValidator + .Setup(o => o.IsCookieTokenValid(testTokenSet.OldCookieToken)) + .Returns(isOldCookieValid); + + mockValidator + .Setup(o => o.IsCookieTokenValid(testTokenSet.NewCookieToken)) + .Returns(!isOldCookieValid); + + return new AntiforgeryWorkerContext() + { + Options = config, + HttpContext = mockHttpContext, + TokenGenerator = mockGenerator, + TokenValidator = mockValidator, + TokenSerializer = mockSerializer, + TokenStore = mockTokenStore, + TestTokenSet = testTokenSet + }; + } + + private class TestTokenSet + { + public AntiforgeryToken FormToken { get; set; } + public string FormTokenString { get; set; } + public AntiforgeryToken OldCookieToken { get; set; } + public string OldCookieTokenString { get; set; } + public AntiforgeryToken NewCookieToken { get; set; } + public string NewCookieTokenString { get; set; } + } + + private class AntiforgeryWorkerContext + { + public AntiforgeryOptions Options { get; set; } + + public TestTokenSet TestTokenSet { get; set; } + + public Mock HttpContext { get; set; } + + public Mock TokenGenerator { get; set; } + + public Mock TokenValidator { get; set; } + + public Mock TokenStore { get; set; } + + public Mock TokenSerializer { get; set; } + } + } +} + +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Antiforgery.Test/BinaryBlobTest.cs b/test/Microsoft.AspNet.Antiforgery.Test/BinaryBlobTest.cs new file mode 100644 index 0000000000..c27b12986d --- /dev/null +++ b/test/Microsoft.AspNet.Antiforgery.Test/BinaryBlobTest.cs @@ -0,0 +1,129 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Xunit; + +namespace Microsoft.AspNet.Antiforgery +{ + public class BinaryBlobTest + { + [Fact] + public void Ctor_BitLength() + { + // Act + var blob = new BinaryBlob(bitLength: 64); + var 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(() => new BinaryBlob(bitLength)); + Assert.Equal("bitLength", ex.ParamName); + } + + [Fact] + public void Ctor_BitLength_ProducesDifferentValues() + { + // Act + var blobA = new BinaryBlob(bitLength: 64); + var blobB = new BinaryBlob(bitLength: 64); + + // Assert + Assert.NotEqual(blobA.GetData(), blobB.GetData()); + } + + [Fact] + public void Ctor_Data() + { + // Arrange + var expectedData = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + + // Act + var 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(() => 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 + var blobData = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + var expectedHashCode = BitConverter.ToInt32(blobData, 0); + + var blob = new BinaryBlob(32, blobData); + + // Act + var actualHashCode = blob.GetHashCode(); + + // Assert + Assert.Equal(expectedHashCode, actualHashCode); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Antiforgery.Test/ClaimUidExtractorTest.cs b/test/Microsoft.AspNet.Antiforgery.Test/ClaimUidExtractorTest.cs new file mode 100644 index 0000000000..1a8dd5c8c7 --- /dev/null +++ b/test/Microsoft.AspNet.Antiforgery.Test/ClaimUidExtractorTest.cs @@ -0,0 +1,115 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Security.Claims; +#if DNX451 +using Moq; +#endif +using Xunit; + +namespace Microsoft.AspNet.Antiforgery +{ + public class ClaimUidExtractorTest + { + [Fact] + public void ExtractClaimUid_NullIdentity() + { + // Arrange + IClaimUidExtractor extractor = new DefaultClaimUidExtractor(); + + // Act + var claimUid = extractor.ExtractClaimUid(null); + + // Assert + Assert.Null(claimUid); + } + +#if DNX451 + [Fact] + public void ExtractClaimUid_Unauthenticated() + { + // Arrange + IClaimUidExtractor extractor = new DefaultClaimUidExtractor(); + + var mockIdentity = new Mock(); + mockIdentity.Setup(o => o.IsAuthenticated) + .Returns(false); + + // Act + var claimUid = extractor.ExtractClaimUid(mockIdentity.Object); + + // Assert + Assert.Null(claimUid); + } + + [Fact] + public void ExtractClaimUid_ClaimsIdentity() + { + // Arrange + var mockIdentity = new Mock(); + mockIdentity.Setup(o => o.IsAuthenticated) + .Returns(true); + + IClaimUidExtractor extractor = new DefaultClaimUidExtractor(); + + // Act + var claimUid = extractor.ExtractClaimUid(mockIdentity.Object); + + // Assert + Assert.NotNull(claimUid); + Assert.Equal("47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", claimUid); + } +#endif + + [Fact] + public void DefaultUniqueClaimTypes_NotPresent_SerializesAllClaimTypes() + { + var identity = new ClaimsIdentity(); + identity.AddClaim(new Claim(ClaimTypes.Email, "someone@antifrogery.com")); + identity.AddClaim(new Claim(ClaimTypes.GivenName, "some")); + identity.AddClaim(new Claim(ClaimTypes.Surname, "one")); +#if DNX451 + // CoreCLR doesn't support an 'empty' name + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, string.Empty)); +#endif + + // Arrange + var claimsIdentity = (ClaimsIdentity)identity; + + // Act + var identiferParameters = DefaultClaimUidExtractor.GetUniqueIdentifierParameters(claimsIdentity) + .ToArray(); + var claims = claimsIdentity.Claims.ToList(); + claims.Sort((a, b) => string.Compare(a.Type, b.Type, StringComparison.Ordinal)); + + // Assert + int index = 0; + foreach (var claim in claims) + { + Assert.Equal(identiferParameters[index++], claim.Type); + Assert.Equal(identiferParameters[index++], claim.Value); + } + } + + [Fact] + public void DefaultUniqueClaimTypes_Present() + { + // Arrange + var identity = new ClaimsIdentity(); + identity.AddClaim(new Claim("fooClaim", "fooClaimValue")); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "nameIdentifierValue")); + + // Act + var uniqueIdentifierParameters = DefaultClaimUidExtractor.GetUniqueIdentifierParameters(identity); + + // Assert + Assert.Equal(new string[] + { + ClaimTypes.NameIdentifier, + "nameIdentifierValue", + }, uniqueIdentifierParameters); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Antiforgery.Test/Microsoft.AspNet.Antiforgery.Test.xproj b/test/Microsoft.AspNet.Antiforgery.Test/Microsoft.AspNet.Antiforgery.Test.xproj index fa6e61dfa1..efe8ba14f1 100644 --- a/test/Microsoft.AspNet.Antiforgery.Test/Microsoft.AspNet.Antiforgery.Test.xproj +++ b/test/Microsoft.AspNet.Antiforgery.Test/Microsoft.AspNet.Antiforgery.Test.xproj @@ -4,7 +4,6 @@ 14.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - 415e83f8-6002-47e4-aa8e-cd5169c06f28 @@ -12,9 +11,11 @@ ..\..\artifacts\obj\$(MSBuildProjectName) ..\..\artifacts\bin\$(MSBuildProjectName)\ - 2.0 + + + - + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Antiforgery.Test/TokenProviderTest.cs b/test/Microsoft.AspNet.Antiforgery.Test/TokenProviderTest.cs new file mode 100644 index 0000000000..ea3955511f --- /dev/null +++ b/test/Microsoft.AspNet.Antiforgery.Test/TokenProviderTest.cs @@ -0,0 +1,588 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Security.Cryptography; +using Microsoft.AspNet.Http; +#if DNX451 +using Moq; +#endif +using Xunit; + +namespace Microsoft.AspNet.Antiforgery +{ + public class TokenProviderTest + { + [Fact] + public void GenerateCookieToken() + { + // Arrange + var tokenProvider = new AntiforgeryTokenProvider( + config: null, + claimUidExtractor: null, + additionalDataProvider: null); + + // Act + var retVal = tokenProvider.GenerateCookieToken(); + + // Assert + Assert.NotNull(retVal); + } + +#if DNX451 + [Fact] + public void GenerateFormToken_AnonymousUser() + { + // Arrange + var cookieToken = new AntiforgeryToken() { IsSessionToken = true }; + var httpContext = new Mock().Object; + var mockIdentity = new Mock(); + mockIdentity.Setup(o => o.IsAuthenticated) + .Returns(false); + + var config = new AntiforgeryOptions(); + + var tokenProvider = new AntiforgeryTokenProvider( + config: config, + claimUidExtractor: null, + additionalDataProvider: null); + + // Act + var fieldToken = tokenProvider.GenerateFormToken(httpContext, mockIdentity.Object, cookieToken); + + // Assert + Assert.NotNull(fieldToken); + Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken); + Assert.False(fieldToken.IsSessionToken); + Assert.Empty(fieldToken.Username); + Assert.Null(fieldToken.ClaimUid); + Assert.Empty(fieldToken.AdditionalData); + } + + [Fact] + public void GenerateFormToken_AuthenticatedWithoutUsernameAndNoAdditionalData_NoAdditionalData() + { + // Arrange + var cookieToken = new AntiforgeryToken() + { + IsSessionToken = true + }; + + var httpContext = new Mock().Object; + ClaimsIdentity identity = new MyAuthenticatedIdentityWithoutUsername(); + var config = new AntiforgeryOptions(); + IClaimUidExtractor claimUidExtractor = new Mock().Object; + + var tokenProvider = new AntiforgeryTokenProvider( + config: config, + claimUidExtractor: claimUidExtractor, + additionalDataProvider: null); + + // Act & assert + var ex = + Assert.Throws( + () => tokenProvider.GenerateFormToken(httpContext, identity, cookieToken)); + Assert.Equal( + "The provided identity of type " + + $"'{typeof(MyAuthenticatedIdentityWithoutUsername).FullName}' " + + "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.", + ex.Message); + } + + [Fact] + public void GenerateFormToken_AuthenticatedWithoutUsername_WithAdditionalData() + { + // Arrange + var cookieToken = new AntiforgeryToken() { IsSessionToken = true }; + var httpContext = new Mock().Object; + ClaimsIdentity identity = new MyAuthenticatedIdentityWithoutUsername(); + + var mockAdditionalDataProvider = new Mock(); + mockAdditionalDataProvider.Setup(o => o.GetAdditionalData(httpContext)) + .Returns("additional-data"); + + var config = new AntiforgeryOptions(); + IClaimUidExtractor claimUidExtractor = new Mock().Object; + + var tokenProvider = new AntiforgeryTokenProvider( + config: config, + claimUidExtractor: claimUidExtractor, + additionalDataProvider: mockAdditionalDataProvider.Object); + + // Act + var fieldToken = tokenProvider.GenerateFormToken(httpContext, identity, cookieToken); + + // Assert + Assert.NotNull(fieldToken); + Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken); + Assert.False(fieldToken.IsSessionToken); + Assert.Empty(fieldToken.Username); + Assert.Null(fieldToken.ClaimUid); + Assert.Equal("additional-data", fieldToken.AdditionalData); + } + + [Fact] + public void GenerateFormToken_ClaimsBasedIdentity() + { + // Arrange + var cookieToken = new AntiforgeryToken() { IsSessionToken = true }; + var httpContext = new Mock().Object; + var identity = GetAuthenticatedIdentity("some-identity"); + + var config = new AntiforgeryOptions(); + + byte[] data = new byte[256 / 8]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(data); + } + var base64ClaimUId = Convert.ToBase64String(data); + var expectedClaimUid = new BinaryBlob(256, data); + + var mockClaimUidExtractor = new Mock(); + mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(identity)) + .Returns(base64ClaimUId); + + var tokenProvider = new AntiforgeryTokenProvider( + config: config, + claimUidExtractor: mockClaimUidExtractor.Object, + additionalDataProvider: null); + + // Act + var fieldToken = tokenProvider.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 + var cookieToken = new AntiforgeryToken() { IsSessionToken = true }; + + var httpContext = new Mock().Object; + var mockIdentity = new Mock(); + mockIdentity.Setup(o => o.IsAuthenticated) + .Returns(true); + mockIdentity.Setup(o => o.Name) + .Returns("my-username"); + + var config = new AntiforgeryOptions(); + IClaimUidExtractor claimUidExtractor = new Mock().Object; + + var tokenProvider = new AntiforgeryTokenProvider( + config: config, + claimUidExtractor: claimUidExtractor, + additionalDataProvider: null); + + // Act + var fieldToken = tokenProvider.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.Null(fieldToken.ClaimUid); + Assert.Empty(fieldToken.AdditionalData); + } +#endif + + [Fact] + public void IsCookieTokenValid_FieldToken_ReturnsFalse() + { + // Arrange + var cookieToken = new AntiforgeryToken() + { + IsSessionToken = false + }; + + var tokenProvider = new AntiforgeryTokenProvider( + config: null, + claimUidExtractor: null, + additionalDataProvider: null); + + // Act + bool retVal = tokenProvider.IsCookieTokenValid(cookieToken); + + // Assert + Assert.False(retVal); + } + + [Fact] + public void IsCookieTokenValid_NullToken_ReturnsFalse() + { + // Arrange + AntiforgeryToken cookieToken = null; + var tokenProvider = new AntiforgeryTokenProvider( + config: null, + claimUidExtractor: null, + additionalDataProvider: null); + + // Act + bool retVal = tokenProvider.IsCookieTokenValid(cookieToken); + + // Assert + Assert.False(retVal); + } + + [Fact] + public void IsCookieTokenValid_ValidToken_ReturnsTrue() + { + // Arrange + var cookieToken = new AntiforgeryToken() + { + IsSessionToken = true + }; + + var tokenProvider = new AntiforgeryTokenProvider( + config: null, + claimUidExtractor: null, + additionalDataProvider: null); + + // Act + bool retVal = tokenProvider.IsCookieTokenValid(cookieToken); + + // Assert + Assert.True(retVal); + } + +#if DNX451 + [Fact] + public void ValidateTokens_SessionTokenMissing() + { + // Arrange + var httpContext = new Mock().Object; + ClaimsIdentity identity = new Mock().Object; + AntiforgeryToken sessionToken = null; + var fieldtoken = new AntiforgeryToken() { IsSessionToken = false }; + + var config = new AntiforgeryOptions() + { + CookieName = "my-cookie-name" + }; + var tokenProvider = new AntiforgeryTokenProvider( + config: config, + claimUidExtractor: null, + additionalDataProvider: null); + + // Act & assert + var ex = + Assert.Throws( + () => tokenProvider.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 + var httpContext = new Mock().Object; + ClaimsIdentity identity = new Mock().Object; + var sessionToken = new AntiforgeryToken() { IsSessionToken = true }; + AntiforgeryToken fieldtoken = null; + + var config = new AntiforgeryOptions() + { + FormFieldName = "my-form-field-name" + }; + + var tokenProvider = new AntiforgeryTokenProvider( + config: config, + claimUidExtractor: null, + additionalDataProvider: null); + + // Act & assert + var ex = + Assert.Throws( + () => tokenProvider.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 + var httpContext = new Mock().Object; + ClaimsIdentity identity = new Mock().Object; + var sessionToken = new AntiforgeryToken() { IsSessionToken = true }; + var fieldtoken = new AntiforgeryToken() { IsSessionToken = false }; + + var config = new AntiforgeryOptions() + { + CookieName = "my-cookie-name", + FormFieldName = "my-form-field-name" + }; + + var tokenProvider = new AntiforgeryTokenProvider( + config: config, + claimUidExtractor: null, + additionalDataProvider: null); + + // Act & assert + var ex1 = + Assert.Throws( + () => tokenProvider.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( + () => tokenProvider.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 + var httpContext = new Mock().Object; + ClaimsIdentity identity = new Mock().Object; + var sessionToken = new AntiforgeryToken() { IsSessionToken = true }; + var fieldtoken = new AntiforgeryToken() { IsSessionToken = false }; + + var tokenProvider = new AntiforgeryTokenProvider( + config: null, + claimUidExtractor: null, + additionalDataProvider: null); + + // Act & assert + var ex = + Assert.Throws( + () => tokenProvider.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 + var httpContext = new Mock().Object; + var identity = GetAuthenticatedIdentity(identityUsername); + var sessionToken = new AntiforgeryToken() { IsSessionToken = true }; + var fieldtoken = new AntiforgeryToken() + { + SecurityToken = sessionToken.SecurityToken, + Username = embeddedUsername, + IsSessionToken = false + }; + + var mockClaimUidExtractor = new Mock(); + mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(identity)) + .Returns((string)null); + + var tokenProvider = new AntiforgeryTokenProvider( + config: null, + claimUidExtractor: mockClaimUidExtractor.Object, + additionalDataProvider: null); + + // Act & assert + var ex = + Assert.Throws( + () => tokenProvider.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 + var httpContext = new Mock().Object; + var identity = GetAuthenticatedIdentity("the-user"); + var sessionToken = new AntiforgeryToken() { IsSessionToken = true }; + var fieldtoken = new AntiforgeryToken() + { + SecurityToken = sessionToken.SecurityToken, + IsSessionToken = false, + ClaimUid = new BinaryBlob(256) + }; + + var differentToken = new BinaryBlob(256); + var mockClaimUidExtractor = new Mock(); + mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(identity)) + .Returns(Convert.ToBase64String(differentToken.GetData())); + + var tokenProvider = new AntiforgeryTokenProvider( + config: null, + claimUidExtractor: mockClaimUidExtractor.Object, + additionalDataProvider: null); + + // Act & assert + var ex = + Assert.Throws( + () => tokenProvider.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 + var httpContext = new Mock().Object; + var identity = new ClaimsIdentity(); + var sessionToken = new AntiforgeryToken() { IsSessionToken = true }; + var fieldtoken = new AntiforgeryToken() + { + SecurityToken = sessionToken.SecurityToken, + Username = String.Empty, + IsSessionToken = false, + AdditionalData = "some-additional-data" + }; + + var mockAdditionalDataProvider = new Mock(); + mockAdditionalDataProvider.Setup(o => o.ValidateAdditionalData(httpContext, "some-additional-data")) + .Returns(false); + + var config = new AntiforgeryOptions(); + var tokenProvider = new AntiforgeryTokenProvider( + config: config, + claimUidExtractor: null, + additionalDataProvider: mockAdditionalDataProvider.Object); + + // Act & assert + var ex = + Assert.Throws( + () => tokenProvider.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 + var httpContext = new Mock().Object; + var identity = new ClaimsIdentity(); + var sessionToken = new AntiforgeryToken() { IsSessionToken = true }; + var fieldtoken = new AntiforgeryToken() + { + SecurityToken = sessionToken.SecurityToken, + Username = String.Empty, + IsSessionToken = false, + AdditionalData = "some-additional-data" + }; + + var mockAdditionalDataProvider = new Mock(); + mockAdditionalDataProvider.Setup(o => o.ValidateAdditionalData(httpContext, "some-additional-data")) + .Returns(true); + + var config = new AntiforgeryOptions(); + var tokenProvider = new AntiforgeryTokenProvider( + config: config, + claimUidExtractor: null, + additionalDataProvider: mockAdditionalDataProvider.Object); + + // Act + tokenProvider.ValidateTokens(httpContext, identity, sessionToken, fieldtoken); + + // Assert + // Nothing to assert - if we got this far, success! + } + + [Fact] + public void ValidateTokens_Success_AuthenticatedUserWithUsername() + { + // Arrange + var httpContext = new Mock().Object; + var identity = GetAuthenticatedIdentity("the-user"); + var sessionToken = new AntiforgeryToken() { IsSessionToken = true }; + var fieldtoken = new AntiforgeryToken() + { + SecurityToken = sessionToken.SecurityToken, + Username = "THE-USER", + IsSessionToken = false, + AdditionalData = "some-additional-data" + }; + + var mockAdditionalDataProvider = new Mock(); + mockAdditionalDataProvider.Setup(o => o.ValidateAdditionalData(httpContext, "some-additional-data")) + .Returns(true); + + var config = new AntiforgeryOptions(); + var tokenProvider = new AntiforgeryTokenProvider( + config: config, + claimUidExtractor: new Mock().Object, + additionalDataProvider: mockAdditionalDataProvider.Object); + + // Act + tokenProvider.ValidateTokens(httpContext, identity, sessionToken, fieldtoken); + + // Assert + // Nothing to assert - if we got this far, success! + } + + [Fact] + public void ValidateTokens_Success_ClaimsBasedUser() + { + // Arrange + var httpContext = new Mock().Object; + var identity = GetAuthenticatedIdentity("the-user"); + var sessionToken = new AntiforgeryToken() { IsSessionToken = true }; + var fieldtoken = new AntiforgeryToken() + { + SecurityToken = sessionToken.SecurityToken, + IsSessionToken = false, + ClaimUid = new BinaryBlob(256) + }; + + var mockClaimUidExtractor = new Mock(); + mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(identity)) + .Returns(Convert.ToBase64String(fieldtoken.ClaimUid.GetData())); + + var config = new AntiforgeryOptions(); + + var tokenProvider = new AntiforgeryTokenProvider( + config: config, + claimUidExtractor: mockClaimUidExtractor.Object, + additionalDataProvider: null); + + // Act + tokenProvider.ValidateTokens(httpContext, identity, sessionToken, fieldtoken); + + // Assert + // Nothing to assert - if we got this far, success! + } +#endif + + private static ClaimsIdentity GetAuthenticatedIdentity(string identityUsername) + { + var claim = new Claim(ClaimsIdentity.DefaultNameClaimType, identityUsername); + return new ClaimsIdentity(new[] { claim }, "Some-Authentication"); + } + + private sealed class MyAuthenticatedIdentityWithoutUsername : ClaimsIdentity + { + public override bool IsAuthenticated + { + get { return true; } + } + + public override string Name + { + get { return String.Empty; } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Antiforgery.Test/project.json b/test/Microsoft.AspNet.Antiforgery.Test/project.json index 8307de87d5..ce82ef7f25 100644 --- a/test/Microsoft.AspNet.Antiforgery.Test/project.json +++ b/test/Microsoft.AspNet.Antiforgery.Test/project.json @@ -1,6 +1,9 @@ { "dependencies": { "Microsoft.AspNet.Antiforgery": "1.0.0-*", + "Microsoft.AspNet.Http": "1.0.0-*", + "Microsoft.Framework.DependencyInjection": "1.0.0-*", + "Microsoft.Framework.WebEncoders.Testing": "1.0.0-*", "xunit.runner.aspnet": "2.0.0-aspnet-*" }, "commands": { @@ -8,7 +11,11 @@ "test": "xunit.runner.aspnet" }, "frameworks": { - "dnx451": { }, + "dnx451": { + "dependencies": { + "Moq": "4.2.1312.1622" + } + }, "dnxcore50": { } } }