Adds a IsRequestValidAsync method

Some other misc cleanup
- docs for IAntiforgeryTokenGenerator
- Add HttpContext parameter where to all IAntiforgeryGenerator methods
- rename parameters on DefaultAntiforgery
This commit is contained in:
Ryan Nowak 2016-02-02 10:37:39 -08:00
parent 0ddfa5f0d8
commit 20140c4c15
9 changed files with 510 additions and 230 deletions

View File

@ -36,75 +36,100 @@ namespace Microsoft.AspNetCore.Antiforgery
}
/// <inheritdoc />
public IHtmlContent GetHtml(HttpContext context)
public IHtmlContent GetHtml(HttpContext httpContext)
{
if (context == null)
if (httpContext == null)
{
throw new ArgumentNullException(nameof(context));
throw new ArgumentNullException(nameof(httpContext));
}
CheckSSLConfig(context);
CheckSSLConfig(httpContext);
var tokenSet = GetAndStoreTokens(context);
var tokenSet = GetAndStoreTokens(httpContext);
return new InputContent(_options.FormFieldName, tokenSet.RequestToken);
}
/// <inheritdoc />
public AntiforgeryTokenSet GetAndStoreTokens(HttpContext context)
public AntiforgeryTokenSet GetAndStoreTokens(HttpContext httpContext)
{
if (context == null)
if (httpContext == null)
{
throw new ArgumentNullException(nameof(context));
throw new ArgumentNullException(nameof(httpContext));
}
CheckSSLConfig(context);
CheckSSLConfig(httpContext);
var tokenSet = GetTokensInternal(context);
var tokenSet = GetTokensInternal(httpContext);
if (tokenSet.IsNewCookieToken)
{
SaveCookieTokenAndHeader(context, tokenSet.CookieToken);
SaveCookieTokenAndHeader(httpContext, tokenSet.CookieToken);
}
return Serialize(tokenSet);
}
/// <inheritdoc />
public AntiforgeryTokenSet GetTokens(HttpContext context)
public AntiforgeryTokenSet GetTokens(HttpContext httpContext)
{
if (context == null)
if (httpContext == null)
{
throw new ArgumentNullException(nameof(context));
throw new ArgumentNullException(nameof(httpContext));
}
CheckSSLConfig(context);
CheckSSLConfig(httpContext);
var tokenSet = GetTokensInternal(context);
var tokenSet = GetTokensInternal(httpContext);
return Serialize(tokenSet);
}
/// <inheritdoc />
public async Task ValidateRequestAsync(HttpContext context)
public async Task<bool> IsRequestValidAsync(HttpContext httpContext)
{
if (context == null)
if (httpContext == null)
{
throw new ArgumentNullException(nameof(context));
throw new ArgumentNullException(nameof(httpContext));
}
CheckSSLConfig(context);
CheckSSLConfig(httpContext);
var tokens = await _tokenStore.GetRequestTokensAsync(context);
ValidateTokens(context, tokens);
var tokens = await _tokenStore.GetRequestTokensAsync(httpContext);
// Extract cookie & request tokens
var deserializedCookieToken = _tokenSerializer.Deserialize(tokens.CookieToken);
var deserializedRequestToken = _tokenSerializer.Deserialize(tokens.RequestToken);
// Validate
string message;
return _tokenGenerator.TryValidateTokenSet(
httpContext,
deserializedCookieToken,
deserializedRequestToken,
out message);
}
/// <inheritdoc />
public void ValidateTokens(HttpContext context, AntiforgeryTokenSet antiforgeryTokenSet)
public async Task ValidateRequestAsync(HttpContext httpContext)
{
if (context == null)
if (httpContext == null)
{
throw new ArgumentNullException(nameof(context));
throw new ArgumentNullException(nameof(httpContext));
}
CheckSSLConfig(context);
CheckSSLConfig(httpContext);
var tokens = await _tokenStore.GetRequestTokensAsync(httpContext);
ValidateTokens(httpContext, tokens);
}
/// <inheritdoc />
public void ValidateTokens(HttpContext httpContext, AntiforgeryTokenSet antiforgeryTokenSet)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
CheckSSLConfig(httpContext);
if (string.IsNullOrEmpty(antiforgeryTokenSet.CookieToken))
{
@ -125,25 +150,30 @@ namespace Microsoft.AspNetCore.Antiforgery
var deserializedRequestToken = _tokenSerializer.Deserialize(antiforgeryTokenSet.RequestToken);
// Validate
_tokenGenerator.ValidateTokens(
context,
string message;
if (!_tokenGenerator.TryValidateTokenSet(
httpContext,
deserializedCookieToken,
deserializedRequestToken);
deserializedRequestToken,
out message))
{
throw new AntiforgeryValidationException(message);
}
}
/// <inheritdoc />
public void SetCookieTokenAndHeader(HttpContext context)
public void SetCookieTokenAndHeader(HttpContext httpContext)
{
if (context == null)
if (httpContext == null)
{
throw new ArgumentNullException(nameof(context));
throw new ArgumentNullException(nameof(httpContext));
}
CheckSSLConfig(context);
CheckSSLConfig(httpContext);
var cookieToken = GetCookieTokenDoesNotThrow(context);
var cookieToken = GetCookieTokenDoesNotThrow(httpContext);
cookieToken = ValidateAndGenerateNewCookieToken(cookieToken);
SaveCookieTokenAndHeader(context, cookieToken);
SaveCookieTokenAndHeader(httpContext, cookieToken);
}
// This method returns null if oldCookieToken is valid.
@ -208,16 +238,16 @@ namespace Microsoft.AspNetCore.Antiforgery
}
}
private AntiforgeryTokenSetInternal GetTokensInternal(HttpContext context)
private AntiforgeryTokenSetInternal GetTokensInternal(HttpContext httpContext)
{
var cookieToken = GetCookieTokenDoesNotThrow(context);
var cookieToken = GetCookieTokenDoesNotThrow(httpContext);
var newCookieToken = ValidateAndGenerateNewCookieToken(cookieToken);
if (newCookieToken != null)
{
cookieToken = newCookieToken;
}
var requestToken = _tokenGenerator.GenerateRequestToken(
context,
httpContext,
cookieToken);
return new AntiforgeryTokenSetInternal()

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.Security.Claims;
using System.Security.Principal;
using Microsoft.AspNetCore.Http;
@ -22,6 +21,7 @@ namespace Microsoft.AspNetCore.Antiforgery
_additionalDataProvider = additionalDataProvider;
}
/// <inheritdoc />
public AntiforgeryToken GenerateCookieToken()
{
return new AntiforgeryToken()
@ -31,6 +31,7 @@ namespace Microsoft.AspNetCore.Antiforgery
};
}
/// <inheritdoc />
public AntiforgeryToken GenerateRequestToken(
HttpContext httpContext,
AntiforgeryToken cookieToken)
@ -40,7 +41,17 @@ namespace Microsoft.AspNetCore.Antiforgery
throw new ArgumentNullException(nameof(httpContext));
}
Debug.Assert(IsCookieTokenValid(cookieToken));
if (cookieToken == null)
{
throw new ArgumentNullException(nameof(cookieToken));
}
if (!IsCookieTokenValid(cookieToken))
{
throw new ArgumentException(
Resources.Antiforgery_CookieToken_IsInvalid,
nameof(cookieToken));
}
var requestToken = new AntiforgeryToken()
{
@ -87,15 +98,18 @@ namespace Microsoft.AspNetCore.Antiforgery
return requestToken;
}
/// <inheritdoc />
public bool IsCookieTokenValid(AntiforgeryToken cookieToken)
{
return (cookieToken != null && cookieToken.IsCookieToken);
return cookieToken != null && cookieToken.IsCookieToken;
}
public void ValidateTokens(
/// <inheritdoc />
public bool TryValidateTokenSet(
HttpContext httpContext,
AntiforgeryToken cookieToken,
AntiforgeryToken requestToken)
AntiforgeryToken requestToken,
out string message)
{
if (httpContext == null)
{
@ -119,13 +133,15 @@ namespace Microsoft.AspNetCore.Antiforgery
// Do the tokens have the correct format?
if (!cookieToken.IsCookieToken || requestToken.IsCookieToken)
{
throw new AntiforgeryValidationException(Resources.AntiforgeryToken_TokensSwapped);
message = Resources.AntiforgeryToken_TokensSwapped;
return false;
}
// Are the security tokens embedded in each incoming token identical?
if (!object.Equals(cookieToken.SecurityToken, requestToken.SecurityToken))
{
throw new AntiforgeryValidationException(Resources.AntiforgeryToken_SecurityTokenMismatch);
message = Resources.AntiforgeryToken_SecurityTokenMismatch;
return false;
}
// Is the incoming token meant for the current user?
@ -153,21 +169,26 @@ namespace Microsoft.AspNetCore.Antiforgery
if (!comparer.Equals(requestToken.Username, currentUsername))
{
throw new AntiforgeryValidationException(
Resources.FormatAntiforgeryToken_UsernameMismatch(requestToken.Username, currentUsername));
message = Resources.FormatAntiforgeryToken_UsernameMismatch(requestToken.Username, currentUsername);
return false;
}
if (!object.Equals(requestToken.ClaimUid, currentClaimUid))
{
throw new AntiforgeryValidationException(Resources.AntiforgeryToken_ClaimUidMismatch);
message = Resources.AntiforgeryToken_ClaimUidMismatch;
return false;
}
// Is the AdditionalData valid?
if (_additionalDataProvider != null &&
!_additionalDataProvider.ValidateAdditionalData(httpContext, requestToken.AdditionalData))
{
throw new AntiforgeryValidationException(Resources.AntiforgeryToken_AdditionalDataCheckFailed);
message = Resources.AntiforgeryToken_AdditionalDataCheckFailed;
return false;
}
message = null;
return true;
}
private static BinaryBlob GetClaimUidBlob(string base64ClaimUid)

View File

@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Antiforgery
/// <summary>
/// Generates an &lt;input type="hidden"&gt; element for an antiforgery token.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> associated with the current request.</param>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
/// <returns>
/// A <see cref="IHtmlContent"/> containing an &lt;input type="hidden"&gt; element. This element should be put
/// inside a &lt;form&gt;.
@ -25,50 +25,63 @@ namespace Microsoft.AspNetCore.Antiforgery
/// This method has a side effect:
/// A response cookie is set if there is no valid cookie associated with the request.
/// </remarks>
IHtmlContent GetHtml(HttpContext context);
IHtmlContent GetHtml(HttpContext httpContext);
/// <summary>
/// Generates an <see cref="AntiforgeryTokenSet"/> for this request and stores the cookie token
/// in the response.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> associated with the current request.</param>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
/// <returns>An <see cref="AntiforgeryTokenSet" /> with tokens for the response.</returns>
/// <remarks>
/// This method has a side effect:
/// A response cookie is set if there is no valid cookie associated with the request.
/// </remarks>
AntiforgeryTokenSet GetAndStoreTokens(HttpContext context);
AntiforgeryTokenSet GetAndStoreTokens(HttpContext httpContext);
/// <summary>
/// Generates an <see cref="AntiforgeryTokenSet"/> for this request.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> associated with the current request.</param>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
/// <remarks>
/// Unlike <see cref="GetAndStoreTokens(HttpContext)"/>, this method has no side effect. The caller
/// is responsible for setting the response cookie and injecting the returned
/// form token as appropriate.
/// </remarks>
AntiforgeryTokenSet GetTokens(HttpContext context);
AntiforgeryTokenSet GetTokens(HttpContext httpContext);
/// <summary>
/// Asynchronously returns a value indicating whether the request contains a valid antiforgery token.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
/// <returns>
/// A <see cref="Task{bool}"/> that, when completed, returns <c>true</c> if the request contains a
/// valid antiforgery token, otherwise returns <c>false</c>.
/// </returns>
Task<bool> IsRequestValidAsync(HttpContext httpContext);
/// <summary>
/// Validates an antiforgery token that was supplied as part of the request.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> associated with the current request.</param>
Task ValidateRequestAsync(HttpContext context);
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
/// <exception cref="AntiforgeryValidationException">
/// Thrown when the request does not include a valid antiforgery token.
/// </exception>
Task ValidateRequestAsync(HttpContext httpContext);
/// <summary>
/// Validates an <see cref="AntiforgeryTokenSet"/> for the current request.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> associated with the current request.</param>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
/// <param name="antiforgeryTokenSet">
/// The <see cref="AntiforgeryTokenSet"/> (cookie and request token) for this request.
/// </param>
void ValidateTokens(HttpContext context, AntiforgeryTokenSet antiforgeryTokenSet);
void ValidateTokens(HttpContext httpContext, AntiforgeryTokenSet antiforgeryTokenSet);
/// <summary>
/// Generates and stores an antiforgery cookie token if one is not available or not valid.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> associated with the current request.</param>
void SetCookieTokenAndHeader(HttpContext context);
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
void SetCookieTokenAndHeader(HttpContext httpContext);
}
}

View File

@ -10,23 +10,41 @@ namespace Microsoft.AspNetCore.Antiforgery
/// </summary>
public interface IAntiforgeryTokenGenerator
{
// Generates a new random cookie token.
/// <summary>
/// Generates a new random cookie token.
/// </summary>
/// <returns>An <see cref="AntiforgeryToken"/>.</returns>
AntiforgeryToken GenerateCookieToken();
// Given a cookie token, generates a corresponding request token.
// The incoming cookie token must be valid.
AntiforgeryToken GenerateRequestToken(
HttpContext httpContext,
AntiforgeryToken cookieToken);
/// <summary>
/// Generates a request token corresponding to <paramref name="cookieToken"/>.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
/// <param name="cookieToken">A valid cookie token.</param>
/// <returns>An <see cref="AntiforgeryToken"/>.</returns>
AntiforgeryToken GenerateRequestToken(HttpContext httpContext, AntiforgeryToken cookieToken);
// Determines whether an existing cookie token is valid (well-formed).
// If it is not, the caller must call GenerateCookieToken() before calling GenerateFormToken().
/// <summary>
/// Attempts to validate a cookie token.
/// </summary>
/// <param name="cookieToken">A valid cookie token.</param>
/// <returns><c>true</c> if the cookie token is valid, otherwise <c>false</c>.</returns>
bool IsCookieTokenValid(AntiforgeryToken cookieToken);
// Validates a (cookie, request) token pair.
void ValidateTokens(
/// <summary>
/// Attempts to validate a cookie and request token set for the given <paramref name="httpContext"/>.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> associated with the current request.</param>
/// <param name="cookieToken">A cookie token.</param>
/// <param name="requestToken">A request token.</param>
/// <param name="message">
/// Will be set to the validation message if the tokens are invalid, otherwise <c>null</c>.
/// </param>
/// <returns><c>true</c> if the tokens are valid, otherwise <c>false</c>.</returns>
bool TryValidateTokenSet(
HttpContext httpContext,
AntiforgeryToken cookieToken,
AntiforgeryToken requestToken);
AntiforgeryToken requestToken,
out string message);
}
}

View File

@ -138,6 +138,14 @@ namespace Microsoft.AspNetCore.Antiforgery
return string.Format(CultureInfo.CurrentCulture, GetString("AntiforgeryWorker_RequireSSL"), p0, p1, p2);
}
/// <summary>
/// The required antiforgery cookie "{0}" is not present.
/// </summary>
internal static string Antiforgery_CookieToken_IsInvalid
{
get { return GetString("Antiforgery_CookieToken_IsInvalid"); }
}
/// <summary>
/// The required antiforgery cookie "{0}" is not present.
/// </summary>

View File

@ -143,6 +143,9 @@
<value>The antiforgery system has the configuration value {0}.{1} = {2}, but the current request is not an SSL request.</value>
<comment>0 = nameof(AntiforgeryOptions), 1 = nameof(RequireSsl), 2 = bool.TrueString</comment>
</data>
<data name="Antiforgery_CookieToken_IsInvalid" xml:space="preserve">
<value>The antiforgery cookie token is invalid.</value>
</data>
<data name="Antiforgery_CookieToken_MustBeProvided" xml:space="preserve">
<value>The required antiforgery cookie "{0}" is not present.</value>
</data>

View File

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.WebEncoders.Testing;
using Moq;
@ -34,9 +35,31 @@ namespace Microsoft.AspNetCore.Antiforgery
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
async () => await antiforgery.ValidateRequestAsync(httpContext));
Assert.Equal(
@"The antiforgery system has the configuration value AntiforgeryOptions.RequireSsl = true, " +
"but the current request is not an SSL request.",
exception.Message);
@"The antiforgery system has the configuration value AntiforgeryOptions.RequireSsl = true, " +
"but the current request is not an SSL request.",
exception.Message);
}
[Fact]
public async Task ChecksSSL_IsRequestValidAsync_Throws()
{
// Arrange
var httpContext = new DefaultHttpContext();
var options = new AntiforgeryOptions()
{
RequireSsl = true
};
var antiforgery = GetAntiforgery(options);
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
async () => await antiforgery.IsRequestValidAsync(httpContext));
Assert.Equal(
@"The antiforgery system has the configuration value AntiforgeryOptions.RequireSsl = true, " +
"but the current request is not an SSL request.",
exception.Message);
}
[Fact]
@ -56,9 +79,9 @@ namespace Microsoft.AspNetCore.Antiforgery
var exception = Assert.Throws<InvalidOperationException>(
() => antiforgery.ValidateTokens(httpContext, new AntiforgeryTokenSet("hello", "world")));
Assert.Equal(
@"The antiforgery system has the configuration value AntiforgeryOptions.RequireSsl = true, " +
"but the current request is not an SSL request.",
exception.Message);
@"The antiforgery system has the configuration value AntiforgeryOptions.RequireSsl = true, " +
"but the current request is not an SSL request.",
exception.Message);
}
[Fact]
@ -162,6 +185,11 @@ namespace Microsoft.AspNetCore.Antiforgery
var antiforgery = GetAntiforgery(context);
var encoder = new HtmlTestEncoder();
// Setup so that the null cookie token returned is treated as invalid.
context.TokenGenerator
.Setup(o => o.IsCookieTokenValid(null))
.Returns(false);
// Act
var inputElement = antiforgery.GetHtml(context.HttpContext);
@ -197,7 +225,7 @@ namespace Microsoft.AspNetCore.Antiforgery
.Setup(o => o.GetCookieToken(context.HttpContext))
.Throws(new Exception("should be swallowed"));
// Setup so that the null cookie token returned is treated as invalid.
// Setup so that the null cookie token returned is treated as invalid.
context.TokenGenerator
.Setup(o => o.IsCookieTokenValid(null))
.Returns(false);
@ -303,12 +331,10 @@ namespace Microsoft.AspNetCore.Antiforgery
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"));
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.TokenGenerator.Setup(o => o.IsCookieTokenValid(null))
.Returns(false);
var antiforgery = GetAntiforgery(context);
// Act
@ -381,7 +407,7 @@ namespace Microsoft.AspNetCore.Antiforgery
}
[Fact]
public void ValidateTokens_FromInvalidStrings_Throws()
public void ValidateTokens_InvalidTokens_Throws()
{
// Arrange
var context = CreateMockContext(new AntiforgeryOptions());
@ -393,17 +419,20 @@ namespace Microsoft.AspNetCore.Antiforgery
.Setup(o => o.Deserialize("form-token"))
.Returns(context.TestTokenSet.RequestToken);
context.TokenGenerator
.Setup(o => o.ValidateTokens(
context.HttpContext,
context.TestTokenSet.OldCookieToken,
context.TestTokenSet.RequestToken))
.Throws(new InvalidOperationException("my-message"));
context.TokenStore = null;
var antiforgery = GetAntiforgery(context);
// You can't really do Moq with out-parameters :(
var tokenGenerator = new TestTokenGenerator()
{
Message = "my-message",
};
// Act & assert
var exception = Assert.Throws<InvalidOperationException>(
var antiforgery = new DefaultAntiforgery(
new TestOptionsManager(),
tokenGenerator,
context.TokenSerializer.Object,
tokenStore: null);
// Act & Assert
var exception = Assert.Throws<AntiforgeryValidationException>(
() => antiforgery.ValidateTokens(
context.HttpContext,
new AntiforgeryTokenSet("form-token", "cookie-token")));
@ -423,11 +452,14 @@ namespace Microsoft.AspNetCore.Antiforgery
.Setup(o => o.Deserialize("form-token"))
.Returns(context.TestTokenSet.RequestToken);
string message;
context.TokenGenerator
.Setup(o => o.ValidateTokens(
.Setup(o => o.TryValidateTokenSet(
context.HttpContext,
context.TestTokenSet.OldCookieToken,
context.TestTokenSet.RequestToken))
context.TestTokenSet.RequestToken,
out message))
.Returns(true)
.Verifiable();
context.TokenStore = null;
var antiforgery = GetAntiforgery(context);
@ -450,12 +482,60 @@ namespace Microsoft.AspNetCore.Antiforgery
// Act
var exception = Assert.Throws<ArgumentException>(
() => antiforgery.ValidateTokens(context.HttpContext, tokenSet));
ExceptionAssert.ThrowsArgument(
() => antiforgery.ValidateTokens(context.HttpContext, tokenSet),
"antiforgeryTokenSet",
"The required antiforgery cookie token must be provided.");
}
[Fact]
public async Task IsRequestValueAsync_FromStore_Failure()
{
// Arrange
var context = CreateMockContext(new AntiforgeryOptions());
string message;
context.TokenGenerator
.Setup(o => o.TryValidateTokenSet(
context.HttpContext,
context.TestTokenSet.OldCookieToken,
context.TestTokenSet.RequestToken,
out message))
.Returns(false);
var antiforgery = GetAntiforgery(context);
// Act
var result = await antiforgery.IsRequestValidAsync(context.HttpContext);
// Assert
var trimmed = exception.Message.Substring(0, exception.Message.IndexOf(Environment.NewLine));
Assert.Equal("The required antiforgery cookie token must be provided.", trimmed);
Assert.False(result);
}
[Fact]
public async Task IsRequestValidAsync_FromStore_Success()
{
// Arrange
var context = CreateMockContext(new AntiforgeryOptions());
string message;
context.TokenGenerator
.Setup(o => o.TryValidateTokenSet(
context.HttpContext,
context.TestTokenSet.OldCookieToken,
context.TestTokenSet.RequestToken,
out message))
.Returns(true)
.Verifiable();
var antiforgery = GetAntiforgery(context);
// Act
var result = await antiforgery.IsRequestValidAsync(context.HttpContext);
// Assert
Assert.True(result);
context.TokenGenerator.Verify();
}
[Fact]
@ -464,17 +544,20 @@ namespace Microsoft.AspNetCore.Antiforgery
// Arrange
var context = CreateMockContext(new AntiforgeryOptions());
context.TokenGenerator
.Setup(o => o.ValidateTokens(
context.HttpContext,
context.TestTokenSet.OldCookieToken,
context.TestTokenSet.RequestToken))
.Throws(new InvalidOperationException("my-message"));
// You can't really do Moq with out-parameters :(
var tokenGenerator = new TestTokenGenerator()
{
Message = "my-message",
};
var antiforgery = GetAntiforgery(context);
var antiforgery = new DefaultAntiforgery(
new TestOptionsManager(),
tokenGenerator,
context.TokenSerializer.Object,
context.TokenStore.Object);
// Act & assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
var exception = await Assert.ThrowsAsync<AntiforgeryValidationException>(
async () => await antiforgery.ValidateRequestAsync(context.HttpContext));
Assert.Equal("my-message", exception.Message);
}
@ -485,11 +568,14 @@ namespace Microsoft.AspNetCore.Antiforgery
// Arrange
var context = CreateMockContext(new AntiforgeryOptions());
string message;
context.TokenGenerator
.Setup(o => o.ValidateTokens(
.Setup(o => o.TryValidateTokenSet(
context.HttpContext,
context.TestTokenSet.OldCookieToken,
context.TestTokenSet.RequestToken))
context.TestTokenSet.RequestToken,
out message))
.Returns(true)
.Verifiable();
var antiforgery = GetAntiforgery(context);
@ -696,5 +782,35 @@ namespace Microsoft.AspNetCore.Antiforgery
{
public AntiforgeryOptions Value { get; set; } = new AntiforgeryOptions();
}
private class TestTokenGenerator : IAntiforgeryTokenGenerator
{
public string Message { get; set; }
public AntiforgeryToken GenerateCookieToken()
{
throw new NotImplementedException();
}
public AntiforgeryToken GenerateRequestToken(HttpContext httpContext, AntiforgeryToken cookieToken)
{
throw new NotImplementedException();
}
public bool IsCookieTokenValid(AntiforgeryToken cookieToken)
{
throw new NotImplementedException();
}
public bool TryValidateTokenSet(
HttpContext httpContext,
AntiforgeryToken cookieToken,
AntiforgeryToken requestToken,
out string message)
{
message = Message;
return false;
}
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Testing;
using Moq;
using Xunit;
@ -28,7 +29,27 @@ namespace Microsoft.AspNetCore.Antiforgery
}
[Fact]
public void GenerateFormToken_AnonymousUser()
public void GenerateRequestToken_InvalidCookieToken()
{
// Arrange
var cookieToken = new AntiforgeryToken() { IsCookieToken = false };
var httpContext = new DefaultHttpContext();
httpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
Assert.False(httpContext.User.Identity.IsAuthenticated);
var tokenProvider = new DefaultAntiforgeryTokenGenerator(
claimUidExtractor: null,
additionalDataProvider: null);
// Act & Assert
ExceptionAssert.ThrowsArgument(
() => tokenProvider.GenerateRequestToken(httpContext, cookieToken),
"cookieToken",
"The antiforgery cookie token is invalid.");
}
[Fact]
public void GenerateRequestToken_AnonymousUser()
{
// Arrange
var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
@ -53,7 +74,7 @@ namespace Microsoft.AspNetCore.Antiforgery
}
[Fact]
public void GenerateFormToken_AuthenticatedWithoutUsernameAndNoAdditionalData_NoAdditionalData()
public void GenerateRequestToken_AuthenticatedWithoutUsernameAndNoAdditionalData_NoAdditionalData()
{
// Arrange
var cookieToken = new AntiforgeryToken()
@ -87,7 +108,7 @@ namespace Microsoft.AspNetCore.Antiforgery
}
[Fact]
public void GenerateFormToken_AuthenticatedWithoutUsername_WithAdditionalData()
public void GenerateRequestToken_AuthenticatedWithoutUsername_WithAdditionalData()
{
// Arrange
var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
@ -118,7 +139,7 @@ namespace Microsoft.AspNetCore.Antiforgery
}
[Fact]
public void GenerateFormToken_ClaimsBasedIdentity()
public void GenerateRequestToken_ClaimsBasedIdentity()
{
// Arrange
var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
@ -156,7 +177,7 @@ namespace Microsoft.AspNetCore.Antiforgery
}
[Fact]
public void GenerateFormToken_RegularUserWithUsername()
public void GenerateRequestToken_RegularUserWithUsername()
{
// Arrange
var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
@ -202,10 +223,10 @@ namespace Microsoft.AspNetCore.Antiforgery
additionalDataProvider: null);
// Act
bool retVal = tokenProvider.IsCookieTokenValid(cookieToken);
var isValid = tokenProvider.IsCookieTokenValid(cookieToken);
// Assert
Assert.False(retVal);
Assert.False(isValid);
}
[Fact]
@ -246,7 +267,7 @@ namespace Microsoft.AspNetCore.Antiforgery
[Fact]
public void ValidateTokens_CookieTokenMissing()
public void TryValidateTokenSet_CookieTokenMissing()
{
// Arrange
var httpContext = new DefaultHttpContext();
@ -254,98 +275,126 @@ namespace Microsoft.AspNetCore.Antiforgery
var fieldtoken = new AntiforgeryToken() { IsCookieToken = false };
var tokenProvider = new DefaultAntiforgeryTokenGenerator(
claimUidExtractor: null,
additionalDataProvider: null);
// Act & assert
var ex = Assert.Throws<ArgumentNullException>(
() => tokenProvider.ValidateTokens(httpContext, null, fieldtoken));
var trimmed = ex.Message.Substring(0, ex.Message.IndexOf(Environment.NewLine));
Assert.Equal(@"The required antiforgery cookie token must be provided.", trimmed);
}
[Fact]
public void ValidateTokens_FieldTokenMissing()
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
var tokenProvider = new DefaultAntiforgeryTokenGenerator(
claimUidExtractor: null,
additionalDataProvider: null);
// Act & assert
var ex = Assert.Throws<ArgumentNullException>(
() => tokenProvider.ValidateTokens(httpContext, cookieToken, null));
var trimmed = ex.Message.Substring(0, ex.Message.IndexOf(Environment.NewLine));
Assert.Equal("The required antiforgery request token must be provided.", trimmed);
}
[Fact]
public void ValidateTokens_FieldAndCookieTokensSwapped()
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
var fieldtoken = new AntiforgeryToken() { IsCookieToken = false };
var tokenProvider = new DefaultAntiforgeryTokenGenerator(
claimUidExtractor: null,
additionalDataProvider: null);
// Act & assert
var ex1 =
Assert.Throws<AntiforgeryValidationException>(
() => tokenProvider.ValidateTokens(httpContext, fieldtoken, fieldtoken));
Assert.Equal(
"Validation of the provided antiforgery token failed. " +
@"The cookie token and the request token were swapped.",
ex1.Message);
var ex2 =
Assert.Throws<AntiforgeryValidationException>(
() => tokenProvider.ValidateTokens(httpContext, cookieToken, cookieToken));
Assert.Equal(
"Validation of the provided antiforgery token failed. " +
@"The cookie token and the request token were swapped.",
ex2.Message);
}
[Fact]
public void ValidateTokens_FieldAndCookieTokensHaveDifferentSecurityKeys()
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
var fieldtoken = new AntiforgeryToken() { IsCookieToken = false };
var tokenProvider = new DefaultAntiforgeryTokenGenerator(
claimUidExtractor: null,
additionalDataProvider: null);
// Act & Assert
var exception = Assert.Throws<AntiforgeryValidationException>(
() => tokenProvider.ValidateTokens(httpContext, cookieToken, fieldtoken));
Assert.Equal(
@"The antiforgery cookie token and request token do not match.",
exception.Message);
string message;
var ex = Assert.Throws<ArgumentNullException>(
() => tokenProvider.TryValidateTokenSet(httpContext, null, fieldtoken, out message));
var trimmed = ex.Message.Substring(0, ex.Message.IndexOf(Environment.NewLine));
Assert.Equal(@"The required antiforgery cookie token must be provided.", trimmed);
}
[Fact]
public void TryValidateTokenSet_FieldTokenMissing()
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
var tokenProvider = new DefaultAntiforgeryTokenGenerator(
claimUidExtractor: null,
additionalDataProvider: null);
// Act & Assert
string message;
var ex = Assert.Throws<ArgumentNullException>(
() => tokenProvider.TryValidateTokenSet(httpContext, cookieToken, null, out message));
var trimmed = ex.Message.Substring(0, ex.Message.IndexOf(Environment.NewLine));
Assert.Equal("The required antiforgery request token must be provided.", trimmed);
}
[Fact]
public void TryValidateTokenSet_FieldAndCookieTokensSwapped_FieldTokenDuplicated()
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
var fieldtoken = new AntiforgeryToken() { IsCookieToken = false };
var tokenProvider = new DefaultAntiforgeryTokenGenerator(
claimUidExtractor: null,
additionalDataProvider: null);
string expectedMessage =
"Validation of the provided antiforgery token failed. " +
"The cookie token and the request token were swapped.";
// Act
string message;
var result = tokenProvider.TryValidateTokenSet(httpContext, fieldtoken, fieldtoken, out message);
// Assert
Assert.False(result);
Assert.Equal(expectedMessage, message);
}
[Fact]
public void TryValidateTokenSet_FieldAndCookieTokensSwapped_CookieDuplicated()
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
var fieldtoken = new AntiforgeryToken() { IsCookieToken = false };
var tokenProvider = new DefaultAntiforgeryTokenGenerator(
claimUidExtractor: null,
additionalDataProvider: null);
string expectedMessage =
"Validation of the provided antiforgery token failed. " +
"The cookie token and the request token were swapped.";
// Act
string message;
var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, cookieToken, out message);
// Assert
Assert.False(result);
Assert.Equal(expectedMessage, message);
}
[Fact]
public void TryValidateTokenSet_FieldAndCookieTokensHaveDifferentSecurityKeys()
{
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
var cookieToken = new AntiforgeryToken() { IsCookieToken = true };
var fieldtoken = new AntiforgeryToken() { IsCookieToken = false };
var tokenProvider = new DefaultAntiforgeryTokenGenerator(
claimUidExtractor: null,
additionalDataProvider: null);
string expectedMessage = "The antiforgery cookie token and request token do not match.";
// Act
string message;
var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message);
// Assert
Assert.False(result);
Assert.Equal(expectedMessage, 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)
public void TryValidateTokenSet_UsernameMismatch(string identityUsername, string embeddedUsername)
{
// Arrange
var httpContext = new DefaultHttpContext();
@ -368,17 +417,21 @@ namespace Microsoft.AspNetCore.Antiforgery
claimUidExtractor: mockClaimUidExtractor.Object,
additionalDataProvider: null);
// Act & Assert
var exception = Assert.Throws<AntiforgeryValidationException>(
() => tokenProvider.ValidateTokens(httpContext, cookieToken, fieldtoken));
Assert.Equal(
@"The provided antiforgery token was meant for user """ + embeddedUsername +
@""", but the current user is """ + identityUsername + @""".",
exception.Message);
string expectedMessage =
$"The provided antiforgery token was meant for user \"{embeddedUsername}\", " +
$"but the current user is \"{identityUsername}\".";
// Act
string message;
var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message);
// Assert
Assert.False(result);
Assert.Equal(expectedMessage, message);
}
[Fact]
public void ValidateTokens_ClaimUidMismatch()
public void TryValidateTokenSet_ClaimUidMismatch()
{
// Arrange
var httpContext = new DefaultHttpContext();
@ -402,16 +455,21 @@ namespace Microsoft.AspNetCore.Antiforgery
claimUidExtractor: mockClaimUidExtractor.Object,
additionalDataProvider: null);
// Act & assert
var exception = Assert.Throws<AntiforgeryValidationException>(
() => tokenProvider.ValidateTokens(httpContext, cookieToken, fieldtoken));
Assert.Equal(
@"The provided antiforgery token was meant for a different claims-based user than the current user.",
exception.Message);
string expectedMessage =
"The provided antiforgery token was meant for a different " +
"claims-based user than the current user.";
// Act
string message;
var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message);
// Assert
Assert.False(result);
Assert.Equal(expectedMessage, message);
}
[Fact]
public void ValidateTokens_AdditionalDataRejected()
public void TryValidateTokenSet_AdditionalDataRejected()
{
// Arrange
var httpContext = new DefaultHttpContext();
@ -428,21 +486,27 @@ namespace Microsoft.AspNetCore.Antiforgery
};
var mockAdditionalDataProvider = new Mock<IAntiforgeryAdditionalDataProvider>();
mockAdditionalDataProvider.Setup(o => o.ValidateAdditionalData(httpContext, "some-additional-data"))
.Returns(false);
mockAdditionalDataProvider
.Setup(o => o.ValidateAdditionalData(httpContext, "some-additional-data"))
.Returns(false);
var tokenProvider = new DefaultAntiforgeryTokenGenerator(
claimUidExtractor: null,
additionalDataProvider: mockAdditionalDataProvider.Object);
// Act & assert
var exception = Assert.Throws<AntiforgeryValidationException>(
() => tokenProvider.ValidateTokens(httpContext, cookieToken, fieldtoken));
Assert.Equal(@"The provided antiforgery token failed a custom data check.", exception.Message);
string expectedMessage = "The provided antiforgery token failed a custom data check.";
// Act
string message;
var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message);
// Assert
Assert.False(result);
Assert.Equal(expectedMessage, message);
}
[Fact]
public void ValidateTokens_Success_AnonymousUser()
public void TryValidateTokenSet_Success_AnonymousUser()
{
// Arrange
var httpContext = new DefaultHttpContext();
@ -467,14 +531,16 @@ namespace Microsoft.AspNetCore.Antiforgery
additionalDataProvider: mockAdditionalDataProvider.Object);
// Act
tokenProvider.ValidateTokens(httpContext, cookieToken, fieldtoken);
string message;
var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message);
// Assert
// Nothing to assert - if we got this far, success!
Assert.True(result);
Assert.Null(message);
}
[Fact]
public void ValidateTokens_Success_AuthenticatedUserWithUsername()
public void TryValidateTokenSet_Success_AuthenticatedUserWithUsername()
{
// Arrange
var httpContext = new DefaultHttpContext();
@ -499,14 +565,16 @@ namespace Microsoft.AspNetCore.Antiforgery
additionalDataProvider: mockAdditionalDataProvider.Object);
// Act
tokenProvider.ValidateTokens(httpContext, cookieToken, fieldtoken);
string message;
var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message);
// Assert
// Nothing to assert - if we got this far, success!
Assert.True(result);
Assert.Null(message);
}
[Fact]
public void ValidateTokens_Success_ClaimsBasedUser()
public void TryValidateTokenSet_Success_ClaimsBasedUser()
{
// Arrange
var httpContext = new DefaultHttpContext();
@ -530,10 +598,12 @@ namespace Microsoft.AspNetCore.Antiforgery
additionalDataProvider: null);
// Act
tokenProvider.ValidateTokens(httpContext, cookieToken, fieldtoken);
string message;
var result = tokenProvider.TryValidateTokenSet(httpContext, cookieToken, fieldtoken, out message);
// Assert
// Nothing to assert - if we got this far, success!
Assert.True(result);
Assert.Null(message);
}
private static ClaimsIdentity GetAuthenticatedIdentity(string identityUsername)

View File

@ -6,6 +6,7 @@
"dependencies": {
"Microsoft.AspNetCore.Antiforgery": "1.0.0-*",
"Microsoft.AspNetCore.Http": "1.0.0-*",
"Microsoft.AspNetCore.Testing": "1.0.0-*",
"Microsoft.Extensions.DependencyInjection": "1.0.0-*",
"Microsoft.Extensions.WebEncoders": "1.0.0-*",
"Microsoft.NETCore.Platforms": "1.0.1-*",