I am trying to implement AntiForgeryToken for my MVC3 Application. I am having a problem with AntiForgeryToken after setting FormAuthentication cookie. Here is a simple exam
The AntiForgeryToken Helper does not add any cookie to response, if a cookie with same name exist in the request. Also the AntiForgeryToken Helper uses Principal.Identity.Name to return a value for hidden field.
AntiForgeryData formToken = new AntiForgeryData(cookieToken) {
Salt = salt,
Username = AntiForgeryData.GetUsername(httpContext.User)
};
So when your Login view uses Html.AntiForgeryToken, a new cookie is set on response and a hidden field with same value. When your Login view post this cookie with hidden field, no exception will be thrown because both request cookie and hidden field value matches. But in the case of About view, no additional cookie will be added to response, but due to IIdentty, a new hidden value will be return for helper. So when you post About action, an exception will raise because cookie and hidden value does not match.
This may be a bug in AntiForgeryToken implementation.
I did some tests, and determined that even after you call FormsAuthentication.SetAuthCookie(...)
, the problem is that httpContext.User.Identity.Name
will still be empty for the duration of the request.
Therefore, to solve this issue, you need to manually set the current User
as so:
FormsAuthentication.SetAuthCookie(email, true);
this.HttpContext.User = new GenericPrincipal(new GenericIdentity(email), null);
This will set the correct User
that is used when Html.AntiForgeryToken()
is called.
Please note that this code isn't necessary for normal PRG-pattern websites, because after the redirect, the correct User
will be loaded.
Also, since your Logon
method requires a valid user name and password, it isn't really susceptible to CSRF attacks, so you probably don't need to use ValidateAntiForgeryToken
on that method. Maybe that's why the AntiForgeryToken is dependent on the user name. CSRF attacks usually only exploit already-authenticated users.
I seem to recall once you login your token is now different as your username I believe changes this token, hence would no longer be valid. I'll try to double check this but almost certain I ran into this in the past.
However in your code above you will run up against other issues if you use this pattern. Post actions aren't generally meant to display a view unless there has been an exception/validation error and you are redisplaying the page. Generally you would redirect. I see someone touched upon this in a comment above and they are correct.
This doesn't mean you shouldn't use those actions though but beware of crossing this over a login. This prior post alludes to the use of username with tokens:
Troubleshooting anti-forgery token problems
public void Validate(HttpContextBase context, string salt) {
Debug.Assert(context != null);
string fieldName = AntiForgeryData.GetAntiForgeryTokenName(null);
string cookieName = AntiForgeryData.GetAntiForgeryTokenName(context.Request.ApplicationPath);
HttpCookie cookie = context.Request.Cookies[cookieName];
if (cookie == null || String.IsNullOrEmpty(cookie.Value)) {
// error: cookie token is missing
throw CreateValidationException();
}
AntiForgeryData cookieToken = Serializer.Deserialize(cookie.Value);
string formValue = context.Request.Form[fieldName];
if (String.IsNullOrEmpty(formValue)) {
// error: form token is missing
throw CreateValidationException();
}
AntiForgeryData formToken = Serializer.Deserialize(formValue);
if (!String.Equals(cookieToken.Value, formToken.Value, StringComparison.Ordinal)) {
// error: form token does not match cookie token
throw CreateValidationException();
}
string currentUsername = AntiForgeryData.GetUsername(context.User);
if (!String.Equals(formToken.Username, currentUsername, StringComparison.OrdinalIgnoreCase)) {
// error: form token is not valid for this user
// (don't care about cookie token)
throw CreateValidationException();
}
if (!String.Equals(salt ?? String.Empty, formToken.Salt, StringComparison.Ordinal)) {
// error: custom validation failed
throw CreateValidationException();
}
}