Move exceptions from the store to the facade

My earlier change to add TryValidateRequestAsync didn't go far enough,
because the store will still throw when the tokens aren't present. This
change is to make the store just return null tokens in these cases, and
move the exceptions to DefaultAntiforgery.
This commit is contained in:
Ryan Nowak 2016-02-04 08:36:35 -08:00
parent c91f0ee667
commit 3a2d09b066
6 changed files with 156 additions and 62 deletions

View File

@ -23,22 +23,13 @@ namespace Microsoft.AspNetCore.Antiforgery
string formFieldName,
string headerName)
{
if (string.IsNullOrEmpty(requestToken))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(requestToken));
}
if (formFieldName == null)
{
throw new ArgumentNullException(nameof(formFieldName));
}
RequestToken = requestToken;
// 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;
FormFieldName = formFieldName;
HeaderName = headerName;
}

View File

@ -93,6 +93,10 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
CheckSSLConfig(httpContext);
var tokens = await _tokenStore.GetRequestTokensAsync(httpContext);
if (tokens.CookieToken == null || tokens.RequestToken == null)
{
return false;
}
// Extract cookie & request tokens
var deserializedCookieToken = _tokenSerializer.Deserialize(tokens.CookieToken);
@ -118,6 +122,33 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
CheckSSLConfig(httpContext);
var tokens = await _tokenStore.GetRequestTokensAsync(httpContext);
if (tokens.CookieToken == null)
{
throw new AntiforgeryValidationException(
Resources.FormatAntiforgery_CookieToken_MustBeProvided(_options.CookieName));
}
if (tokens.RequestToken == null)
{
if (_options.HeaderName == null)
{
var message = Resources.FormatAntiforgery_FormToken_MustBeProvided(_options.FormFieldName);
throw new AntiforgeryValidationException(message);
}
else if (!httpContext.Request.HasFormContentType)
{
var message = Resources.FormatAntiforgery_HeaderToken_MustBeProvided(_options.HeaderName);
throw new AntiforgeryValidationException(message);
}
else
{
var message = Resources.FormatAntiforgery_RequestToken_MustBeProvided(
_options.FormFieldName,
_options.HeaderName);
throw new AntiforgeryValidationException(message);
}
}
ValidateTokens(httpContext, tokens);
}
@ -262,8 +293,8 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
private AntiforgeryTokenSet Serialize(AntiforgeryTokenSetInternal tokenSet)
{
return new AntiforgeryTokenSet(
tokenSet.RequestToken != null ? _tokenSerializer.Serialize(tokenSet.RequestToken) : null,
tokenSet.CookieToken != null ? _tokenSerializer.Serialize(tokenSet.CookieToken) : null,
_tokenSerializer.Serialize(tokenSet.RequestToken),
_tokenSerializer.Serialize(tokenSet.CookieToken),
_options.FormFieldName,
_options.HeaderName);
}

View File

@ -65,18 +65,13 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
throw new ArgumentNullException(nameof(httpContext));
}
var requestCookie = httpContext.Request.Cookies[_options.CookieName];
if (string.IsNullOrEmpty(requestCookie))
{
throw new AntiforgeryValidationException(
Resources.FormatAntiforgery_CookieToken_MustBeProvided(_options.CookieName));
}
var cookieToken = httpContext.Request.Cookies[_options.CookieName];
StringValues requestToken;
if (httpContext.Request.HasFormContentType)
{
// Check the content-type before accessing the form collection to make sure
// we throw gracefully.
// we report errors gracefully.
var form = await httpContext.Request.ReadFormAsync();
requestToken = form[_options.FormFieldName];
}
@ -87,28 +82,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
requestToken = httpContext.Request.Headers[_options.HeaderName];
}
if (requestToken.Count == 0)
{
if (_options.HeaderName == null)
{
var message = Resources.FormatAntiforgery_FormToken_MustBeProvided(_options.FormFieldName);
throw new AntiforgeryValidationException(message);
}
else if (!httpContext.Request.HasFormContentType)
{
var message = Resources.FormatAntiforgery_HeaderToken_MustBeProvided(_options.HeaderName);
throw new AntiforgeryValidationException(message);
}
else
{
var message = Resources.FormatAntiforgery_RequestToken_MustBeProvided(
_options.FormFieldName,
_options.HeaderName);
throw new AntiforgeryValidationException(message);
}
}
return new AntiforgeryTokenSet(requestToken, requestCookie, _options.FormFieldName, _options.HeaderName);
return new AntiforgeryTokenSet(requestToken, cookieToken, _options.FormFieldName, _options.HeaderName);
}
public void SaveCookieToken(HttpContext httpContext, AntiforgeryToken token)

View File

@ -6,14 +6,12 @@ using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Antiforgery.Internal
{
// Provides an abstraction around how tokens are persisted and retrieved for a request
public interface IAntiforgeryTokenStore
{
AntiforgeryToken GetCookieToken(HttpContext httpContext);
/// <summary>
/// Gets the cookie and request tokens from the request. Will throw an exception if either token is
/// not present.
/// Gets the cookie and request tokens from the request.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> for the current request.</param>
/// <returns>The <see cref="AntiforgeryTokenSet"/>.</returns>

View File

@ -591,6 +591,109 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
context.TokenGenerator.Verify();
}
[Fact]
public async Task ValidateRequestAsync_NoCookieToken_Throws()
{
// Arrange
var context = CreateMockContext(new AntiforgeryOptions()
{
CookieName = "cookie-name",
FormFieldName = "form-field-name",
HeaderName = null,
});
var tokenSet = new AntiforgeryTokenSet(null, null, "form-field-name", null);
context.TokenStore
.Setup(s => s.GetRequestTokensAsync(context.HttpContext))
.Returns(Task.FromResult(tokenSet));
var antiforgery = GetAntiforgery(context);
// Act & Assert
var exception = await Assert.ThrowsAsync<AntiforgeryValidationException>(
() => antiforgery.ValidateRequestAsync(context.HttpContext));
Assert.Equal("The required antiforgery cookie \"cookie-name\" is not present.", exception.Message);
}
[Fact]
public async Task ValidateRequestAsync_NonFormRequest_HeaderDisabled_Throws()
{
// Arrange
var context = CreateMockContext(new AntiforgeryOptions()
{
CookieName = "cookie-name",
FormFieldName = "form-field-name",
HeaderName = null,
});
var tokenSet = new AntiforgeryTokenSet(null, "cookie-token", "form-field-name", null);
context.TokenStore
.Setup(s => s.GetRequestTokensAsync(context.HttpContext))
.Returns(Task.FromResult(tokenSet));
var antiforgery = GetAntiforgery(context);
// Act & Assert
var exception = await Assert.ThrowsAsync<AntiforgeryValidationException>(
() => antiforgery.ValidateRequestAsync(context.HttpContext));
Assert.Equal("The required antiforgery form field \"form-field-name\" is not present.", exception.Message);
}
[Fact]
public async Task ValidateRequestAsync_NonFormRequest_NoHeaderValue_Throws()
{
// Arrange
var context = CreateMockContext(new AntiforgeryOptions()
{
CookieName = "cookie-name",
FormFieldName = "form-field-name",
HeaderName = "header-name",
});
context.HttpContext.Request.ContentType = "application/json";
var tokenSet = new AntiforgeryTokenSet(null, "cookie-token", "form-field-name", "header-name");
context.TokenStore
.Setup(s => s.GetRequestTokensAsync(context.HttpContext))
.Returns(Task.FromResult(tokenSet));
var antiforgery = GetAntiforgery(context);
// Act & Assert
var exception = await Assert.ThrowsAsync<AntiforgeryValidationException>(
() => antiforgery.ValidateRequestAsync(context.HttpContext));
Assert.Equal("The required antiforgery header value \"header-name\" is not present.", exception.Message);
}
[Fact]
public async Task ValidateRequestAsync_FormRequest_NoRequestTokenValue_Throws()
{
// Arrange
var context = CreateMockContext(new AntiforgeryOptions()
{
CookieName = "cookie-name",
FormFieldName = "form-field-name",
HeaderName = "header-name",
});
context.HttpContext.Request.ContentType = "application/x-www-form-urlencoded";
var tokenSet = new AntiforgeryTokenSet(null, "cookie-token", "form-field-name", "header-name");
context.TokenStore
.Setup(s => s.GetRequestTokensAsync(context.HttpContext))
.Returns(Task.FromResult(tokenSet));
var antiforgery = GetAntiforgery(context);
// Act & Assert
var exception = await Assert.ThrowsAsync<AntiforgeryValidationException>(
() => antiforgery.ValidateRequestAsync(context.HttpContext));
Assert.Equal(
"The required antiforgery request token was not provided in either form field \"form-field-name\" " +
"or header value \"header-name\".",
exception.Message);
}
[Theory]
[InlineData(false, "SAMEORIGIN")]
[InlineData(true, null)]

View File

@ -145,7 +145,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
}
[Fact]
public async Task GetRequestTokens_CookieIsEmpty_Throws()
public async Task GetRequestTokens_CookieIsEmpty_ReturnsNullTokens()
{
// Arrange
var httpContext = GetHttpContext(new RequestCookieCollection());
@ -162,15 +162,15 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
tokenSerializer: Mock.Of<IAntiforgeryTokenSerializer>());
// Act
var exception = await Assert.ThrowsAsync<AntiforgeryValidationException>(
async () => await tokenStore.GetRequestTokensAsync(httpContext));
var tokenSet = await tokenStore.GetRequestTokensAsync(httpContext);
// Assert
Assert.Equal("The required antiforgery cookie \"cookie-name\" is not present.", exception.Message);
Assert.Null(tokenSet.CookieToken);
Assert.Null(tokenSet.RequestToken);
}
[Fact]
public async Task GetRequestTokens_NonFormContentType_HeaderDisabled_Throws()
public async Task GetRequestTokens_NonFormContentType_HeaderDisabled_ReturnsNullToken()
{
// Arrange
var httpContext = GetHttpContext("cookie-name", "cookie-value");
@ -191,11 +191,11 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
tokenSerializer: new DefaultAntiforgeryTokenSerializer(new EphemeralDataProtectionProvider(), _pool));
// Act
var exception = await Assert.ThrowsAsync<AntiforgeryValidationException>(
async () => await tokenStore.GetRequestTokensAsync(httpContext));
var tokenSet = await tokenStore.GetRequestTokensAsync(httpContext);
// Assert
Assert.Equal("The required antiforgery form field \"form-field-name\" is not present.", exception.Message);
Assert.Equal("cookie-value", tokenSet.CookieToken);
Assert.Null(tokenSet.RequestToken);
}
[Fact]
@ -257,7 +257,7 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
}
[Fact]
public async Task GetRequestTokens_NonFormContentType_UsesHeaderToken_ThrowsOnMissingValue()
public async Task GetRequestTokens_NonFormContentType_NoHeaderToken_ReturnsNullToken()
{
// Arrange
var httpContext = GetHttpContext("cookie-name", "cookie-value");
@ -278,15 +278,15 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
tokenSerializer: new DefaultAntiforgeryTokenSerializer(new EphemeralDataProtectionProvider(), _pool));
// Act
var exception = await Assert.ThrowsAsync<AntiforgeryValidationException>(
async () => await tokenStore.GetRequestTokensAsync(httpContext));
var tokenSet = await tokenStore.GetRequestTokensAsync(httpContext);
// Assert
Assert.Equal("The required antiforgery header value \"header-name\" is not present.", exception.Message);
Assert.Equal("cookie-value", tokenSet.CookieToken);
Assert.Null(tokenSet.RequestToken);
}
[Fact]
public async Task GetRequestTokens_BothFieldsEmpty_Throws()
public async Task GetRequestTokens_BothFieldsEmpty_ReturnsNullTokens()
{
// Arrange
var httpContext = GetHttpContext("cookie-name", "cookie-value");
@ -305,14 +305,11 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
tokenSerializer: Mock.Of<IAntiforgeryTokenSerializer>());
// Act
var exception = await Assert.ThrowsAsync<AntiforgeryValidationException>(
async () => await tokenStore.GetRequestTokensAsync(httpContext));
var tokenSet = await tokenStore.GetRequestTokensAsync(httpContext);
// Assert
Assert.Equal(
"The required antiforgery request token was not provided in either form field \"form-field-name\" " +
"or header value \"header-name\".",
exception.Message);
Assert.Equal("cookie-value", tokenSet.CookieToken);
Assert.Null(tokenSet.RequestToken);
}
[Fact]