From bbafa0a29a71ae3f6d22b8dcd794d21dd02f9e7a Mon Sep 17 00:00:00 2001 From: harshgMSFT Date: Mon, 21 Apr 2014 19:33:13 -0700 Subject: [PATCH] Tests for AntiForgery System. --- .../AntiForgery/AntiForgeryTokenSet.cs | 3 + .../AntiForgery/AntiForgeryWorker.cs | 1 + .../AntiForgery/DefaultClaimUidExtractor.cs | 2 +- .../Properties/AssemblyInfo.cs | 1 + .../Properties/Resources.Designer.cs | 4 +- src/Microsoft.AspNet.Mvc.Core/Resources.resx | 2 +- .../AntiForgeryTokenSerializerTest.cs | 198 ++++++ .../AntiXsrf/AntiForgeryTokenStoreTest.cs | 366 +++++++++++ .../AntiXsrf/AntiForgeryTokenTest.cs | 146 +++++ .../AntiXsrf/AntiForgeryWorkerTests.cs | 539 ++++++++++++++++ .../AntiXsrf/BinaryBlobTest.cs | 143 +++++ .../AntiXsrf/ClaimUidExtractorTest.cs | 122 ++++ .../AntiXsrf/ITokenProvider.cs | 24 + .../AntiXsrf/MockAntiForgeryConfig.cs | 46 ++ .../AntiXsrf/MockClaimsIdentity.cs | 39 ++ .../AntiXsrf/TokenProviderTests.cs | 588 ++++++++++++++++++ .../ValidateAntiForgeryTokenAttributeTest.cs | 55 ++ .../Microsoft.AspNet.Mvc.Core.Test.kproj | 12 + .../Properties/AssemblyInfo.cs | 20 + .../project.json | 1 + 20 files changed, 2308 insertions(+), 4 deletions(-) create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/AntiForgeryTokenSerializerTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/AntiForgeryTokenStoreTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/AntiForgeryTokenTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/AntiForgeryWorkerTests.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/BinaryBlobTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/ClaimUidExtractorTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/ITokenProvider.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/MockAntiForgeryConfig.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/MockClaimsIdentity.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/TokenProviderTests.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/ValidateAntiForgeryTokenAttributeTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Properties/AssemblyInfo.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryTokenSet.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryTokenSet.cs index 677302483b..0e30159e92 100644 --- a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryTokenSet.cs +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryTokenSet.cs @@ -30,6 +30,9 @@ namespace Microsoft.AspNet.Mvc } 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; } diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryWorker.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryWorker.cs index 6d7a38ff67..875ee4fb86 100644 --- a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryWorker.cs +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/AntiForgeryWorker.cs @@ -177,6 +177,7 @@ namespace Microsoft.AspNet.Mvc return new AntiForgeryTokenSetInternal() { + // Note : The new cookie would be null if the old cookie is valid. CookieToken = newCookieToken, FormToken = formToken }; diff --git a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/DefaultClaimUidExtractor.cs b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/DefaultClaimUidExtractor.cs index 90a01108fe..71fb3afa1f 100644 --- a/src/Microsoft.AspNet.Mvc.Core/AntiForgery/DefaultClaimUidExtractor.cs +++ b/src/Microsoft.AspNet.Mvc.Core/AntiForgery/DefaultClaimUidExtractor.cs @@ -40,7 +40,7 @@ namespace Microsoft.AspNet.Mvc return Convert.ToBase64String(claimUidBytes); } - private static IEnumerable GetUniqueIdentifierParameters(ClaimsIdentity claimsIdentity) + internal static IEnumerable GetUniqueIdentifierParameters(ClaimsIdentity claimsIdentity) { // TODO: Need to enable support for special casing acs identities. var nameIdentifierClaim = claimsIdentity.FindFirst(claim => diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/AssemblyInfo.cs index f124dd377f..dc3478099d 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/AssemblyInfo.cs @@ -52,3 +52,4 @@ using System.Runtime.InteropServices; [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: InternalsVisibleTo("Microsoft.AspNet.Mvc.Core.Test")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index 3e1168b50b..04bfed9fb5 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -76,7 +76,7 @@ namespace Microsoft.AspNet.Mvc.Core } /// - /// The anti-forgery token could not be decrypted. If this application is hosted by a Web Farm or cluster, ensure that all machines are running the same version of ASP.NET Web Pages and that the <machineKey> configuration specifies explicit encryption and validation keys. AutoGenerate cannot be used in a cluster. + /// The anti-forgery token could not be decrypted. /// internal static string AntiForgeryToken_DeserializationFailed { @@ -84,7 +84,7 @@ namespace Microsoft.AspNet.Mvc.Core } /// - /// The anti-forgery token could not be decrypted. If this application is hosted by a Web Farm or cluster, ensure that all machines are running the same version of ASP.NET Web Pages and that the <machineKey> configuration specifies explicit encryption and validation keys. AutoGenerate cannot be used in a cluster. + /// The anti-forgery token could not be decrypted. /// internal static string FormatAntiForgeryToken_DeserializationFailed() { diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index 55b2f2eeb3..92c39ca690 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -127,7 +127,7 @@ The required anti-forgery cookie "{0}" is not present. - The anti-forgery token could not be decrypted. If this application is hosted by a Web Farm or cluster, ensure that all machines are running the same version of ASP.NET Web Pages and that the <machineKey> configuration specifies explicit encryption and validation keys. AutoGenerate cannot be used in a cluster. + The anti-forgery token could not be decrypted. The required anti-forgery form field "{0}" is not present. diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/AntiForgeryTokenSerializerTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/AntiForgeryTokenSerializerTest.cs new file mode 100644 index 0000000000..aaf73e20ab --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/AntiForgeryTokenSerializerTest.cs @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Security.Claims; +using System.Text; +using Microsoft.AspNet.Security.DataProtection; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Core.Test +{ + 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); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/AntiForgeryTokenStoreTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/AntiForgeryTokenStoreTest.cs new file mode 100644 index 0000000000..9a4784a265 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/AntiForgeryTokenStoreTest.cs @@ -0,0 +1,366 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Core.Test +{ + 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 config = new MockAntiForgeryConfig() + { + 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_CookieIsEmpty_ReturnsNull() + { + // Arrange + var mockHttpContext = GetMockHttpContext(_cookieName, string.Empty); + + var config = new MockAntiForgeryConfig() + { + 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 MockAntiForgeryConfig() + { + 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"); + + MockAntiForgeryConfig config = new MockAntiForgeryConfig() + { + 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(); + IReadableStringCollection formsCollection = + new MockCookieCollection(new Dictionary() { { "form-field-name", string.Empty } }); + requestContext.Setup(o => o.GetFormAsync()) + .Returns(Task.FromResult(formsCollection)); + mockHttpContext.Setup(o => o.Request) + .Returns(requestContext.Object); + + var config = new MockAntiForgeryConfig() + { + 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 + IReadableStringCollection formsCollection = + new MockCookieCollection(new Dictionary() { { "form-field-name", "invalid-value" } }); + + var requestContext = new Mock(); + requestContext.Setup(o => o.GetFormAsync()) + .Returns(Task.FromResult(formsCollection)); + + var mockHttpContext = new Mock(); + mockHttpContext.Setup(o => o.Request) + .Returns(requestContext.Object); + + var config = new MockAntiForgeryConfig() + { + 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(); + IReadableStringCollection formsCollection = + new MockCookieCollection(new Dictionary() { { "form-field-name", "valid-value" } }); + requestContext.Setup(o => o.GetFormAsync()) + .Returns(Task.FromResult(formsCollection)); + mockHttpContext.Setup(o => o.Request) + .Returns(requestContext.Object); + + var config = new MockAntiForgeryConfig() + { + 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(); + + // TODO : Once we decide on where to pick this value from enable this. + 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 mockSerializer = new Mock(); + mockSerializer.Setup(o => o.Serialize(token)) + .Returns("serialized-value"); + + var config = new MockAntiForgeryConfig() + { + 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(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); + + return mockHttpContext.Object; + } + + 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 MockCookieCollection(Dictionary dictionary) + { + this.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 string this[string key] + { + get { return this.dictionary[key]; } + } + + public IEnumerator> GetEnumerator() + { + throw new NotImplementedException(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/AntiForgeryTokenTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/AntiForgeryTokenTest.cs new file mode 100644 index 0000000000..a1d0657c1f --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/AntiForgeryTokenTest.cs @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +using Xunit; + +namespace Microsoft.AspNet.Mvc.Core.Test +{ + 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.Mvc.Core.Test/AntiXsrf/AntiForgeryWorkerTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/AntiForgeryWorkerTests.cs new file mode 100644 index 0000000000..d56ebb3121 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/AntiForgeryWorkerTests.cs @@ -0,0 +1,539 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Security.Principal; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.PipelineCore.Collections; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Core.Test +{ + public class AntiForgeryWorkerTest + { + + [Fact] + public async Task ChecksSSL_ValidateAsync_Throws() + { + // Arrange + var mockHttpContext = new Mock(); + mockHttpContext.Setup(o => o.Request.IsSecure) + .Returns(false); + + var config = new MockAntiForgeryConfig() + { + RequireSSL = true + }; + + var worker = new AntiForgeryWorker( + config: config, + serializer: null, + tokenStore: null, + generator: null, + validator: null); + + // Act & assert + var ex = + await + Assert.ThrowsAsync( + async () => await worker.ValidateAsync(mockHttpContext.Object)); + Assert.Equal( + @"The anti-forgery system has the configuration value AntiForgeryConfig.RequireSsl = true, " + + "but the current request is not an SSL request.", + ex.Message); + } + + [Fact] + public void ChecksSSL_Validate_Throws() + { + // Arrange + var mockHttpContext = new Mock(); + mockHttpContext.Setup(o => o.Request.IsSecure) + .Returns(false); + + var config = new MockAntiForgeryConfig() + { + RequireSSL = true + }; + + var worker = new AntiForgeryWorker( + config: config, + serializer: null, + tokenStore: null, + generator: null, + validator: null); + + // Act & assert + var ex = Assert.Throws( + () => worker.Validate(mockHttpContext.Object, cookieToken: null, formToken: null)); + Assert.Equal( + @"The anti-forgery system has the configuration value AntiForgeryConfig.RequireSsl = true, " + + "but the current request is not an SSL request.", + ex.Message); + } + + [Fact] + public void ChecksSSL_GetFormInputElement_Throws() + { + // Arrange + var mockHttpContext = new Mock(); + mockHttpContext.Setup(o => o.Request.IsSecure) + .Returns(false); + + var config = new MockAntiForgeryConfig() + { + RequireSSL = true + }; + + var worker = new AntiForgeryWorker( + config: config, + serializer: null, + tokenStore: null, + generator: null, + validator: null); + + // Act & assert + var ex = Assert.Throws(() => worker.GetFormInputElement(mockHttpContext.Object)); + Assert.Equal( + @"The anti-forgery system has the configuration value AntiForgeryConfig.RequireSsl = true, " + + "but the current request is not an SSL request.", + ex.Message); + } + + [Fact] + public void ChecksSSL_GetTokens_Throws() + { + // Arrange + var mockHttpContext = new Mock(); + mockHttpContext.Setup(o => o.Request.IsSecure) + .Returns(false); + + var config = new MockAntiForgeryConfig() + { + RequireSSL = true + }; + + var worker = new AntiForgeryWorker( + config: config, + serializer: null, + tokenStore: null, + generator: null, + validator: null); + + // Act & assert + var ex = Assert.Throws(() => worker.GetTokens(mockHttpContext.Object, "cookie-token")); + Assert.Equal( + @"The anti-forgery system has the configuration value AntiForgeryConfig.RequireSsl = true, " + + "but the current request is not an SSL request.", + ex.Message); + } + + [Fact] + public void GetFormInputElement_ExistingInvalidCookieToken_GeneratesANewCookieAndAnAntiForgeryToken() + { + // Arrange + var config = new MockAntiForgeryConfig() + { + 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.ToString(TagRenderMode.SelfClosing)); + context.TokenStore.Verify(); + } + + [Fact] + public void GetFormInputElement_ExistingInvalidCookieToken_SwallowsExceptions() + { + // Arrange + var config = new MockAntiForgeryConfig() + { + 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.TokenProvider.Setup(o => o.IsCookieTokenValid(null)) + .Returns(false); + + // Act + var inputElement = worker.GetFormInputElement(context.HttpContext.Object); + + // Assert + Assert.Equal(@"", + inputElement.ToString(TagRenderMode.SelfClosing)); + context.TokenStore.Verify(); + } + + [Fact] + public void GetFormInputElement_ExistingValidCookieToken_GeneratesAnAntiForgeryToken() + { + // Arrange + var config = new MockAntiForgeryConfig() + { + FormFieldName = "form-field-name" + }; + + // Make sure the existing cookie is valid and use the same cookie for the mock Token Provider. + var context = GetAntiForgeryWorkerContext(config, useOldCookie: true, isOldCookieValid: true); + var worker = GetAntiForgeryWorker(context); + + // Act + var inputElement = worker.GetFormInputElement(context.HttpContext.Object); + + // Assert + Assert.Equal(@"", + inputElement.ToString(TagRenderMode.SelfClosing)); + } + + [Theory] + [InlineData(false, "SAMEORIGIN")] + [InlineData(true, null)] + public void GetFormInputElement_AddsXFrameOptionsHeader(bool suppressXFrameOptions, string expectedHeaderValue) + { + // Arrange + var config = new MockAntiForgeryConfig() + { + SuppressXFrameOptionsHeader = suppressXFrameOptions + }; + + // Genreate a new cookie. + var context = GetAntiForgeryWorkerContext(config, 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 MockAntiForgeryConfig(), 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 MockAntiForgeryConfig(), 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.TokenProvider.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 MockAntiForgeryConfig(), 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 MockAntiForgeryConfig()); + + 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.TokenProvider.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 MockAntiForgeryConfig()); + + 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.TokenProvider.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.TokenProvider.Verify(); + } + + [Fact] + public async Task Validate_FromStore_Failure() + { + // Arrange + var context = GetAntiForgeryWorkerContext(new MockAntiForgeryConfig()); + + context.TokenProvider.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 MockAntiForgeryConfig()); + + context.TokenProvider.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.TokenProvider.Verify(); + } + + private AntiForgeryWorker GetAntiForgeryWorker(AntiForgeryWorkerContext context) + { + return new AntiForgeryWorker( + config: context.Config, + serializer: context.TokenSerializer != null ? context.TokenSerializer.Object : null, + tokenStore: context.TokenStore != null ? context.TokenStore.Object : null, + generator: context.TokenProvider != null ? context.TokenProvider.Object : null, + validator: context.TokenProvider != null ? context.TokenProvider.Object : null); + } + + private Mock GetHttpContext(bool setupResponse = true) + { + var identity = new GenericIdentity("some-user"); + var mockHttpContext = new Mock(); + + mockHttpContext.Setup(o => o.User) + .Returns(new GenericPrincipal(identity, new string[0])); + + + 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 GetTokenProvider(HttpContext context, TestTokenSet testTokenSet, bool useOldCookie, bool isOldCookieValid = true, bool isNewCookieValid = true) + { + var oldCookieToken = testTokenSet.OldCookieToken; + var newCookieToken = testTokenSet.NewCookieToken; + var formToken = testTokenSet.FormToken; + var mockValidator = new Mock(MockBehavior.Strict); + mockValidator.Setup(o => o.GenerateFormToken(context, context.User.Identity as ClaimsIdentity, useOldCookie ? oldCookieToken : newCookieToken)) + .Returns(formToken); + mockValidator.Setup(o => o.IsCookieTokenValid(oldCookieToken)) + .Returns(isOldCookieValid); + mockValidator.Setup(o => o.IsCookieTokenValid(newCookieToken)) + .Returns(isNewCookieValid); + + mockValidator.Setup(o => o.GenerateCookieToken()) + .Returns(useOldCookie ? oldCookieToken : newCookieToken); + + return mockValidator; + } + + 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(MockAntiForgeryConfig 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 mockTokenProvider = GetTokenProvider(mockHttpContext.Object, testTokenSet, useOldCookie: useOldCookie, isOldCookieValid: isOldCookieValid); + + return new AntiForgeryWorkerContext() + { + Config = config, + HttpContext = mockHttpContext, + TokenProvider = mockTokenProvider, + 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 MockAntiForgeryConfig Config { get; set; } + + public TestTokenSet TestTokenSet { get; set; } + + public Mock HttpContext { get; set; } + + public Mock TokenProvider { get; set; } + + public Mock TokenStore { get; set; } + + public Mock TokenSerializer { get; set; } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/BinaryBlobTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/BinaryBlobTest.cs new file mode 100644 index 0000000000..93ed8ffdbe --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/BinaryBlobTest.cs @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +using System; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Core.Test +{ + 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.Mvc.Core.Test/AntiXsrf/ClaimUidExtractorTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/ClaimUidExtractorTest.cs new file mode 100644 index 0000000000..e3eec68760 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/ClaimUidExtractorTest.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +using System; +using System.Linq; +using System.Security.Claims; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Core.Test +{ + public class ClaimUidExtractorTest + { + [Fact] + public void ExtractClaimUid_NullIdentity() + { + // Arrange + IClaimUidExtractor extractor = new DefaultClaimUidExtractor(); + + // Act + var claimUid = extractor.ExtractClaimUid(null); + + // Assert + Assert.Null(claimUid); + } + + [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); + } + + [Fact] + public void DefaultUniqueClaimTypes_NotPresent_SerializesAllClaimTypes() + { + var identity = new MockClaimsIdentity(); + identity.AddClaim(ClaimTypes.Email, "someone@antifrogery.com"); + identity.AddClaim(ClaimTypes.GivenName, "some"); + identity.AddClaim(ClaimTypes.Surname, "one"); + identity.AddClaim(ClaimTypes.NameIdentifier, String.Empty); + + // 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.True(String.Equals(identiferParameters[index++], claim.Type, StringComparison.Ordinal)); + Assert.True(String.Equals(identiferParameters[index++], claim.Value, StringComparison.Ordinal)); + } + } + + [Fact] + public void DefaultUniqueClaimTypes_Present() + { + // Arrange + var identity = new MockClaimsIdentity(); + identity.AddClaim("fooClaim", "fooClaimValue"); + identity.AddClaim(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.Mvc.Core.Test/AntiXsrf/ITokenProvider.cs b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/ITokenProvider.cs new file mode 100644 index 0000000000..842bcce239 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/ITokenProvider.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +namespace Microsoft.AspNet.Mvc.Core.Test +{ + // A TokenProvider that can be passed to MoQ + internal interface ITokenProvider : ITokenValidator, ITokenGenerator + { + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/MockAntiForgeryConfig.cs b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/MockAntiForgeryConfig.cs new file mode 100644 index 0000000000..73ee9f8b45 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/MockAntiForgeryConfig.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +namespace Microsoft.AspNet.Mvc.Core.Test +{ + public sealed class MockAntiForgeryConfig : IAntiForgeryConfig + { + public string CookieName + { + get; + set; + } + + public string FormFieldName + { + get; + set; + } + + public bool RequireSSL + { + get; + set; + } + + public bool SuppressXFrameOptionsHeader + { + get; + set; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/MockClaimsIdentity.cs b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/MockClaimsIdentity.cs new file mode 100644 index 0000000000..d857a0e97a --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/MockClaimsIdentity.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +using System.Collections.Generic; +using System.Security.Claims; + +namespace Microsoft.AspNet.Mvc.Core.Test +{ + // Convenient class for mocking a ClaimsIdentity instance given some + // prefabricated Claim instances. + internal sealed class MockClaimsIdentity : ClaimsIdentity + { + private readonly List _claims = new List(); + + public void AddClaim(string claimType, string value) + { + _claims.Add(new Claim(claimType, value)); + } + + public override IEnumerable Claims + { + get { return _claims; } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/TokenProviderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/TokenProviderTests.cs new file mode 100644 index 0000000000..2982bd1073 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/TokenProviderTests.cs @@ -0,0 +1,588 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +using System; +using System.Security.Claims; +using System.Security.Principal; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Security.DataProtection; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Core.Test +{ + public class TokenProviderTest + { + [Fact] + public void GenerateCookieToken() + { + // Arrange + var tokenProvider = new TokenProvider( + config: null, + claimUidExtractor: null, + additionalDataProvider: null); + + // Act + var retVal = tokenProvider.GenerateCookieToken(); + + // Assert + Assert.NotNull(retVal); + } + + [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); + + IAntiForgeryConfig config = new MockAntiForgeryConfig(); + + var tokenProvider = new TokenProvider( + 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.Equal("", fieldToken.Username); + Assert.Equal(null, fieldToken.ClaimUid); + Assert.Equal("", fieldToken.AdditionalData); + } + + [Fact] + public void GenerateFormToken_AuthenticatedWithoutUsernameAndNoAdditionalData_NoAdditionalData() + { + // Arrange + var cookieToken = new AntiForgeryToken() + { + IsSessionToken = true + }; + + var httpContext = new Mock().Object; + ClaimsIdentity identity = new MyAuthenticatedIdentityWithoutUsername(); + IAntiForgeryConfig config = new MockAntiForgeryConfig(); + IClaimUidExtractor claimUidExtractor = new Mock().Object; + + var tokenProvider = new TokenProvider( + config: config, + claimUidExtractor: claimUidExtractor, + additionalDataProvider: null); + + // Act & assert + var ex = + Assert.Throws( + () => tokenProvider.GenerateFormToken(httpContext, identity, cookieToken)); + Assert.Equal( + "The provided identity of type "+ + "'Microsoft.AspNet.Mvc.Core.Test.TokenProviderTest+MyAuthenticatedIdentityWithoutUsername' "+ + "is marked IsAuthenticated = true but does not have a value for Name. "+ + "By default, the anti-forgery system requires that all authenticated identities have a unique Name. " + + "If it is not possible to provide a unique Name for this identity, " + + "consider 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"); + + IAntiForgeryConfig config = new AntiForgeryConfigWrapper(); + IClaimUidExtractor claimUidExtractor = new Mock().Object; + + var tokenProvider = new TokenProvider( + 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.Equal("", fieldToken.Username); + Assert.Equal(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; + ClaimsIdentity identity = new GenericIdentity("some-identity"); + + var config = new MockAntiForgeryConfig(); + + byte[] data = new byte[256 / 8]; + CryptRand.FillBuffer(new ArraySegment(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 TokenProvider( + 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"); + + IAntiForgeryConfig config = new MockAntiForgeryConfig(); + IClaimUidExtractor claimUidExtractor = new Mock().Object; + + var tokenProvider = new TokenProvider( + 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.Equal(null, fieldToken.ClaimUid); + Assert.Equal("", fieldToken.AdditionalData); + } + + [Fact] + public void IsCookieTokenValid_FieldToken_ReturnsFalse() + { + // Arrange + var cookieToken = new AntiForgeryToken() + { + IsSessionToken = false + }; + + var tokenProvider = new TokenProvider( + 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 TokenProvider( + 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 TokenProvider( + config: null, + claimUidExtractor: null, + additionalDataProvider: null); + + // Act + bool retVal = tokenProvider.IsCookieTokenValid(cookieToken); + + // Assert + Assert.True(retVal); + } + + [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 MockAntiForgeryConfig() + { + CookieName = "my-cookie-name" + }; + var tokenProvider = new TokenProvider( + 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 MockAntiForgeryConfig() + { + FormFieldName = "my-form-field-name" + }; + + var tokenProvider = new TokenProvider( + 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 MockAntiForgeryConfig() + { + CookieName = "my-cookie-name", + FormFieldName = "my-form-field-name" + }; + + var tokenProvider = new TokenProvider( + 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 TokenProvider( + 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; + ClaimsIdentity identity = new GenericIdentity(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 TokenProvider( + 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; + ClaimsIdentity identity = new GenericIdentity("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 TokenProvider( + 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; + ClaimsIdentity identity = new GenericIdentity(String.Empty); + 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 MockAntiForgeryConfig(); + var tokenProvider = new TokenProvider( + 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; + ClaimsIdentity identity = new GenericIdentity(String.Empty); + 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 MockAntiForgeryConfig(); + var tokenProvider = new TokenProvider( + 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; + ClaimsIdentity identity = new GenericIdentity("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 MockAntiForgeryConfig(); + var tokenProvider = new TokenProvider( + 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; + ClaimsIdentity identity = new GenericIdentity("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 MockAntiForgeryConfig(); + + var tokenProvider = new TokenProvider( + config: config, + claimUidExtractor: mockClaimUidExtractor.Object, + additionalDataProvider: null); + + // Act + tokenProvider.ValidateTokens(httpContext, identity, sessionToken, fieldtoken); + + // Assert + // Nothing to assert - if we got this far, success! + } + + 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.Mvc.Core.Test/AntiXsrf/ValidateAntiForgeryTokenAttributeTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/ValidateAntiForgeryTokenAttributeTest.cs new file mode 100644 index 0000000000..35c0b068af --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/AntiXsrf/ValidateAntiForgeryTokenAttributeTest.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; +using Microsoft.AspNet.Security.DataProtection; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Core.Test +{ + public class ValidateAntiForgeryTokenAttributeTest + { + [Fact] + public void ValidationAttribute_ForwardsCallToValidateAntiForgeryTokenAuthorizationFilter() + { + // Arrange + var serviceCollection = new ServiceCollection(); + serviceCollection.AddInstance(GetAntiForgeryInstance()); + var serviceProvider = serviceCollection.BuildServiceProvider(); + var attribute = new ValidateAntiForgeryTokenAttribute(); + + // Act + var filter = attribute.CreateInstance(serviceProvider); + + // Assert + var validationFilter = filter as ValidateAntiForgeryTokenAuthorizationFilter; + Assert.NotNull(validationFilter); + } + + private AntiForgery GetAntiForgeryInstance() + { + var claimExtractor = new Mock(); + var dataProtectionProvider = new Mock(); + var additionalDataProvider = new Mock(); + return new AntiForgery(claimExtractor.Object, + dataProtectionProvider.Object, + additionalDataProvider.Object); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj index e42800a6ab..20eb0fdabd 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj @@ -26,12 +26,24 @@ + + + + + + + + + + + + diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Properties/AssemblyInfo.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..e28ab11a7d --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/project.json b/test/Microsoft.AspNet.Mvc.Core.Test/project.json index 25d1f17db7..b6c0f7f085 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/project.json +++ b/test/Microsoft.AspNet.Mvc.Core.Test/project.json @@ -5,6 +5,7 @@ }, "dependencies": { "Microsoft.AspNet.Http": "0.1-alpha-*", + "Microsoft.AspNet.PipelineCore": "0.1-alpha-*", "Microsoft.AspNet.Mvc.Core" : "", "Microsoft.AspNet.Mvc" : "", "Microsoft.AspNet.Routing": "0.1-alpha-*",