// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. using System.Security.Principal; using System.Web.Helpers.Test; using System.Web.Mvc; using Moq; using Xunit; using Xunit.Extensions; using Assert = Microsoft.TestCommon.AssertEx; namespace System.Web.Helpers.AntiXsrf.Test { public class TokenValidatorTest { [Fact] public void GenerateCookieToken() { // Arrange TokenValidator tokenValidator = new TokenValidator( config: null, claimUidExtractor: null); // Act AntiForgeryToken retVal = tokenValidator.GenerateCookieToken(); // Assert Assert.NotNull(retVal); } [Fact] public void GenerateFormToken_AnonymousUser() { // Arrange AntiForgeryToken cookieToken = new AntiForgeryToken() { IsSessionToken = true }; HttpContextBase httpContext = new Mock().Object; Mock mockIdentity = new Mock(); mockIdentity.Setup(o => o.IsAuthenticated).Returns(false); IAntiForgeryConfig config = new MockAntiForgeryConfig(); TokenValidator validator = new TokenValidator( config: config, claimUidExtractor: null); // Act var fieldToken = validator.GenerateFormToken(httpContext, mockIdentity.Object, cookieToken); // Assert Assert.NotNull(fieldToken); Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken); Assert.False(fieldToken.IsSessionToken); Assert.Equal("", fieldToken.Username); Assert.Equal(null, fieldToken.ClaimUid); Assert.Equal("", fieldToken.AdditionalData); } [Fact] public void GenerateFormToken_AuthenticatedWithoutUsernameAndNoAdditionalData_NoAdditionalData() { // Arrange AntiForgeryToken cookieToken = new AntiForgeryToken() { IsSessionToken = true }; HttpContextBase httpContext = new Mock().Object; IIdentity identity = new MyAuthenticatedIdentityWithoutUsername(); IAntiForgeryConfig config = new MockAntiForgeryConfig(); IClaimUidExtractor claimUidExtractor = new Mock().Object; TokenValidator validator = new TokenValidator( config: config, claimUidExtractor: claimUidExtractor); // Act & assert var ex = Assert.Throws(() => validator.GenerateFormToken(httpContext, identity, cookieToken)); Assert.Equal(@"The provided identity of type 'System.Web.Helpers.AntiXsrf.Test.TokenValidatorTest+MyAuthenticatedIdentityWithoutUsername' is marked IsAuthenticated = true but does not have a value for Name. By default, the anti-forgery system requires that all authenticated identities have a unique Name. If it is not possible to provide a unique Name for this identity, consider setting the static property AntiForgeryConfig.AdditionalDataProvider to an instance of a type that can provide some form of unique identifier for the current user.", ex.Message); } [Fact] public void GenerateFormToken_AuthenticatedWithoutUsernameAndNoAdditionalData_NoAdditionalData_SuppressHeuristics() { // Arrange AntiForgeryToken cookieToken = new AntiForgeryToken() { IsSessionToken = true }; HttpContextBase httpContext = new Mock().Object; IIdentity identity = new MyAuthenticatedIdentityWithoutUsername(); IAntiForgeryConfig config = new MockAntiForgeryConfig() { SuppressIdentityHeuristicChecks = true }; IClaimUidExtractor claimUidExtractor = new Mock().Object; TokenValidator validator = new TokenValidator( config: config, claimUidExtractor: claimUidExtractor); // Act var fieldToken = validator.GenerateFormToken(httpContext, identity, cookieToken); // Assert Assert.NotNull(fieldToken); Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken); Assert.False(fieldToken.IsSessionToken); Assert.Equal("", fieldToken.Username); Assert.Equal(null, fieldToken.ClaimUid); Assert.Equal("", fieldToken.AdditionalData); } [Fact] public void GenerateFormToken_AuthenticatedWithoutUsername_WithAdditionalData() { // Arrange AntiForgeryToken cookieToken = new AntiForgeryToken() { IsSessionToken = true }; HttpContextBase httpContext = new Mock().Object; IIdentity identity = new MyAuthenticatedIdentityWithoutUsername(); Mock mockAdditionalDataProvider = new Mock(); mockAdditionalDataProvider.Setup(o => o.GetAdditionalData(httpContext)).Returns("additional-data"); IAntiForgeryConfig config = new MockAntiForgeryConfig() { AdditionalDataProvider = mockAdditionalDataProvider.Object }; IClaimUidExtractor claimUidExtractor = new Mock().Object; TokenValidator validator = new TokenValidator( config: config, claimUidExtractor: claimUidExtractor); // Act var fieldToken = validator.GenerateFormToken(httpContext, identity, cookieToken); // Assert Assert.NotNull(fieldToken); Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken); Assert.False(fieldToken.IsSessionToken); Assert.Equal("", fieldToken.Username); Assert.Equal(null, fieldToken.ClaimUid); Assert.Equal("additional-data", fieldToken.AdditionalData); } [Fact] public void GenerateFormToken_ClaimsBasedIdentity() { // Arrange AntiForgeryToken cookieToken = new AntiForgeryToken() { IsSessionToken = true }; HttpContextBase httpContext = new Mock().Object; IIdentity identity = new GenericIdentity("some-identity"); MockAntiForgeryConfig config = new MockAntiForgeryConfig() { UniqueClaimTypeIdentifier = "unique-identifier" }; BinaryBlob expectedClaimUid = new BinaryBlob(256); Mock mockClaimUidExtractor = new Mock(); mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(identity)).Returns((object)expectedClaimUid); TokenValidator validator = new TokenValidator( config: config, claimUidExtractor: mockClaimUidExtractor.Object); // Act var fieldToken = validator.GenerateFormToken(httpContext, identity, cookieToken); // Assert Assert.NotNull(fieldToken); Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken); Assert.False(fieldToken.IsSessionToken); Assert.Equal("", fieldToken.Username); Assert.Equal(expectedClaimUid, fieldToken.ClaimUid); Assert.Equal("", fieldToken.AdditionalData); } [Fact] public void GenerateFormToken_RegularUserWithUsername() { // Arrange AntiForgeryToken cookieToken = new AntiForgeryToken() { IsSessionToken = true }; HttpContextBase httpContext = new Mock().Object; Mock 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; TokenValidator validator = new TokenValidator( config: config, claimUidExtractor: claimUidExtractor); // Act var fieldToken = validator.GenerateFormToken(httpContext, mockIdentity.Object, cookieToken); // Assert Assert.NotNull(fieldToken); Assert.Equal(cookieToken.SecurityToken, fieldToken.SecurityToken); Assert.False(fieldToken.IsSessionToken); Assert.Equal("my-username", fieldToken.Username); Assert.Equal(null, fieldToken.ClaimUid); Assert.Equal("", fieldToken.AdditionalData); } [Fact] public void IsCookieTokenValid_FieldToken_ReturnsFalse() { // Arrange AntiForgeryToken cookieToken = new AntiForgeryToken() { IsSessionToken = false }; TokenValidator validator = new TokenValidator( config: null, claimUidExtractor: null); // Act bool retVal = validator.IsCookieTokenValid(cookieToken); // Assert Assert.False(retVal); } [Fact] public void IsCookieTokenValid_NullToken_ReturnsFalse() { // Arrange AntiForgeryToken cookieToken = null; TokenValidator validator = new TokenValidator( config: null, claimUidExtractor: null); // Act bool retVal = validator.IsCookieTokenValid(cookieToken); // Assert Assert.False(retVal); } [Fact] public void IsCookieTokenValid_ValidToken_ReturnsTrue() { // Arrange AntiForgeryToken cookieToken = new AntiForgeryToken() { IsSessionToken = true }; TokenValidator validator = new TokenValidator( config: null, claimUidExtractor: null); // Act bool retVal = validator.IsCookieTokenValid(cookieToken); // Assert Assert.True(retVal); } [Fact] public void ValidateTokens_SessionTokenMissing() { // Arrange HttpContextBase httpContext = new Mock().Object; IIdentity identity = new Mock().Object; AntiForgeryToken sessionToken = null; AntiForgeryToken fieldtoken = new AntiForgeryToken() { IsSessionToken = false }; MockAntiForgeryConfig config = new MockAntiForgeryConfig() { CookieName = "my-cookie-name" }; TokenValidator validator = new TokenValidator( config: config, claimUidExtractor: null); // Act & assert var ex = Assert.Throws(() => validator.ValidateTokens(httpContext, identity, sessionToken, fieldtoken)); Assert.Equal(@"The required anti-forgery cookie ""my-cookie-name"" is not present.", ex.Message); } [Fact] public void ValidateTokens_FieldTokenMissing() { // Arrange HttpContextBase httpContext = new Mock().Object; IIdentity identity = new Mock().Object; AntiForgeryToken sessionToken = new AntiForgeryToken() { IsSessionToken = true }; AntiForgeryToken fieldtoken = null; MockAntiForgeryConfig config = new MockAntiForgeryConfig() { FormFieldName = "my-form-field-name" }; TokenValidator validator = new TokenValidator( config: config, claimUidExtractor: null); // Act & assert var ex = Assert.Throws(() => validator.ValidateTokens(httpContext, identity, sessionToken, fieldtoken)); Assert.Equal(@"The required anti-forgery form field ""my-form-field-name"" is not present.", ex.Message); } [Fact] public void ValidateTokens_FieldAndSessionTokensSwapped() { // Arrange HttpContextBase httpContext = new Mock().Object; IIdentity identity = new Mock().Object; AntiForgeryToken sessionToken = new AntiForgeryToken() { IsSessionToken = true }; AntiForgeryToken fieldtoken = new AntiForgeryToken() { IsSessionToken = false }; MockAntiForgeryConfig config = new MockAntiForgeryConfig() { CookieName = "my-cookie-name", FormFieldName = "my-form-field-name" }; TokenValidator validator = new TokenValidator( config: config, claimUidExtractor: null); // Act & assert var ex1 = Assert.Throws(() => validator.ValidateTokens(httpContext, identity, fieldtoken, fieldtoken)); Assert.Equal(@"Validation of the provided anti-forgery token failed. The cookie ""my-cookie-name"" and the form field ""my-form-field-name"" were swapped.", ex1.Message); var ex2 = Assert.Throws(() => validator.ValidateTokens(httpContext, identity, sessionToken, sessionToken)); Assert.Equal(@"Validation of the provided anti-forgery token failed. The cookie ""my-cookie-name"" and the form field ""my-form-field-name"" were swapped.", ex2.Message); } [Fact] public void ValidateTokens_FieldAndSessionTokensHaveDifferentSecurityKeys() { // Arrange HttpContextBase httpContext = new Mock().Object; IIdentity identity = new Mock().Object; AntiForgeryToken sessionToken = new AntiForgeryToken() { IsSessionToken = true }; AntiForgeryToken fieldtoken = new AntiForgeryToken() { IsSessionToken = false }; TokenValidator validator = new TokenValidator( config: null, claimUidExtractor: null); // Act & assert var ex = Assert.Throws(() => validator.ValidateTokens(httpContext, identity, sessionToken, fieldtoken)); Assert.Equal(@"The anti-forgery cookie token and form field token do not match.", ex.Message); } [Theory] [InlineData("the-user", "the-other-user")] [InlineData("http://example.com/uri-casing", "http://example.com/URI-casing")] [InlineData("https://example.com/secure-uri-casing", "https://example.com/secure-URI-casing")] public void ValidateTokens_UsernameMismatch(string identityUsername, string embeddedUsername) { // Arrange HttpContextBase httpContext = new Mock().Object; IIdentity identity = new GenericIdentity(identityUsername); AntiForgeryToken sessionToken = new AntiForgeryToken() { IsSessionToken = true }; AntiForgeryToken fieldtoken = new AntiForgeryToken() { SecurityToken = sessionToken.SecurityToken, Username = embeddedUsername, IsSessionToken = false }; Mock mockClaimUidExtractor = new Mock(); mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(identity)).Returns((object)null); TokenValidator validator = new TokenValidator( config: null, claimUidExtractor: mockClaimUidExtractor.Object); // Act & assert var ex = Assert.Throws(() => validator.ValidateTokens(httpContext, identity, sessionToken, fieldtoken)); Assert.Equal(@"The provided anti-forgery token was meant for user """ + embeddedUsername + @""", but the current user is """ + identityUsername + @""".", ex.Message); } [Fact] public void ValidateTokens_ClaimUidMismatch() { // Arrange HttpContextBase httpContext = new Mock().Object; IIdentity identity = new GenericIdentity("the-user"); AntiForgeryToken sessionToken = new AntiForgeryToken() { IsSessionToken = true }; AntiForgeryToken fieldtoken = new AntiForgeryToken() { SecurityToken = sessionToken.SecurityToken, IsSessionToken = false, ClaimUid = new BinaryBlob(256) }; Mock mockClaimUidExtractor = new Mock(); mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(identity)).Returns(new BinaryBlob(256)); TokenValidator validator = new TokenValidator( config: null, claimUidExtractor: mockClaimUidExtractor.Object); // Act & assert var ex = Assert.Throws(() => validator.ValidateTokens(httpContext, identity, sessionToken, fieldtoken)); Assert.Equal(@"The provided anti-forgery token was meant for a different claims-based user than the current user.", ex.Message); } [Fact] public void ValidateTokens_AdditionalDataRejected() { // Arrange HttpContextBase httpContext = new Mock().Object; IIdentity identity = new GenericIdentity(String.Empty); AntiForgeryToken sessionToken = new AntiForgeryToken() { IsSessionToken = true }; AntiForgeryToken fieldtoken = new AntiForgeryToken() { SecurityToken = sessionToken.SecurityToken, Username = String.Empty, IsSessionToken = false, AdditionalData = "some-additional-data" }; Mock mockAdditionalDataProvider = new Mock(); mockAdditionalDataProvider.Setup(o => o.ValidateAdditionalData(httpContext, "some-additional-data")).Returns(false); MockAntiForgeryConfig config = new MockAntiForgeryConfig() { AdditionalDataProvider = mockAdditionalDataProvider.Object }; TokenValidator validator = new TokenValidator( config: config, claimUidExtractor: null); // Act & assert var ex = Assert.Throws(() => validator.ValidateTokens(httpContext, identity, sessionToken, fieldtoken)); Assert.Equal(@"The provided anti-forgery token failed a custom data check.", ex.Message); } [Fact] public void ValidateTokens_Success_AnonymousUser() { // Arrange HttpContextBase httpContext = new Mock().Object; IIdentity identity = new GenericIdentity(String.Empty); AntiForgeryToken sessionToken = new AntiForgeryToken() { IsSessionToken = true }; AntiForgeryToken fieldtoken = new AntiForgeryToken() { SecurityToken = sessionToken.SecurityToken, Username = String.Empty, IsSessionToken = false, AdditionalData = "some-additional-data" }; Mock mockAdditionalDataProvider = new Mock(); mockAdditionalDataProvider.Setup(o => o.ValidateAdditionalData(httpContext, "some-additional-data")).Returns(true); MockAntiForgeryConfig config = new MockAntiForgeryConfig() { AdditionalDataProvider = mockAdditionalDataProvider.Object }; TokenValidator validator = new TokenValidator( config: config, claimUidExtractor: null); // Act validator.ValidateTokens(httpContext, identity, sessionToken, fieldtoken); // Assert // Nothing to assert - if we got this far, success! } [Fact] public void ValidateTokens_Success_AuthenticatedUserWithUsername() { // Arrange HttpContextBase httpContext = new Mock().Object; IIdentity identity = new GenericIdentity("the-user"); AntiForgeryToken sessionToken = new AntiForgeryToken() { IsSessionToken = true }; AntiForgeryToken fieldtoken = new AntiForgeryToken() { SecurityToken = sessionToken.SecurityToken, Username = "THE-USER", IsSessionToken = false, AdditionalData = "some-additional-data" }; Mock mockAdditionalDataProvider = new Mock(); mockAdditionalDataProvider.Setup(o => o.ValidateAdditionalData(httpContext, "some-additional-data")).Returns(true); MockAntiForgeryConfig config = new MockAntiForgeryConfig() { AdditionalDataProvider = mockAdditionalDataProvider.Object }; TokenValidator validator = new TokenValidator( config: config, claimUidExtractor: new Mock().Object); // Act validator.ValidateTokens(httpContext, identity, sessionToken, fieldtoken); // Assert // Nothing to assert - if we got this far, success! } [Fact] public void ValidateTokens_Success_ClaimsBasedUser() { // Arrange HttpContextBase httpContext = new Mock().Object; IIdentity identity = new GenericIdentity("the-user"); AntiForgeryToken sessionToken = new AntiForgeryToken() { IsSessionToken = true }; AntiForgeryToken fieldtoken = new AntiForgeryToken() { SecurityToken = sessionToken.SecurityToken, IsSessionToken = false, ClaimUid = new BinaryBlob(256) }; Mock mockClaimUidExtractor = new Mock(); mockClaimUidExtractor.Setup(o => o.ExtractClaimUid(identity)).Returns(fieldtoken.ClaimUid); MockAntiForgeryConfig config = new MockAntiForgeryConfig(); TokenValidator validator = new TokenValidator( config: config, claimUidExtractor: mockClaimUidExtractor.Object); // Act validator.ValidateTokens(httpContext, identity, sessionToken, fieldtoken); // Assert // Nothing to assert - if we got this far, success! } private sealed class MyAuthenticatedIdentityWithoutUsername : IIdentity { public string AuthenticationType { get { throw new NotImplementedException(); } } public bool IsAuthenticated { get { return true; } } public string Name { get { return String.Empty; } } } } }