I have a site that is a relying party to our WIF-based custom STS. We recently implemented a Security Token Cache as described here: Azure/web-farm ready SecurityTokenCache. The major difference between our implementation and the one described in that link is that we use Azure AppFabric Caching as the backing store for the durable cache, rather than table storage. This helped to relieve us of a token truncation issue on certain browsers but has introduced a new problem (We see the truncation problem primarily on pages that have google analytics + antiforgery cookies in addition to the fedauth cookie). We're now receiving the following exception several thousand times per day:
System.IdentityModel.Tokens.SecurityTokenException
ID4243: Could not create a SecurityToken. A token was not found in the token cache and no cookie was found in the context.
System.IdentityModel.Tokens.SecurityTokenException: ID4243: Could not create a SecurityToken. A token was not found in the token cache and no cookie was found in the context.
at Microsoft.IdentityModel.Tokens.SessionSecurityTokenHandler.ReadToken(XmlReader reader, SecurityTokenResolver tokenResolver)
at Microsoft.IdentityModel.Tokens.SessionSecurityTokenHandler.ReadToken(Byte[] token, SecurityTokenResolver tokenResolver)
at Microsoft.IdentityModel.Web.SessionAuthenticationModule.ReadSessionTokenFromCookie(Byte[] sessionCookie)
at Microsoft.IdentityModel.Web.SessionAuthenticationModule.TryReadSessionTokenFromCookie(SessionSecurityToken& sessionToken)
at Microsoft.IdentityModel.Web.SessionAuthenticationModule.OnAuthenticateRequest(Object sender, EventArgs eventArgs)
at System.Web.HttpApplication.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)
This exception seems to be happening in a redirect loop, so we'll see hundreds of them within a 1-2 minute time span.
I've been unable to locate any useful information while researching the exception. The only nugget that holds any hope so far is someone mentioning that it may be related to the cached object expiring prior to the session.
We've been unable to reproduce the problem internally and only know it exists because of the thousands of entries filling up our Elmah tables. Any help or insight would be very much appreciated.
We pushed out what we thought may help resolve the problem (code below) but it had no effect:
HttpContext.Current.Response.Cookies.Remove("FedAuth");
WSFederationAuthenticationModule authModule = FederatedAuthentication.WSFederationAuthenticationModule;
string signoutUrl = (WSFederationAuthenticationModule.GetFederationPassiveSignOutUrl(authModule.Issuer, authModule.Realm, null));
Response.Redirect(signoutUrl);
I have an MVC single page application as a relying party using WSO2 4.5 as the IDP and was getting the same error - "System.IdentityModel.Tokens.SecurityTokenException
ID4243: Could not create a SecurityToken. A token was not found in the token cache and no cookie was found in the context. ..." Did a search and found the statements below by Brock Allen of Thinktecture fame.
This exception is thrown when the browser is sending a cookie that contains the user’s claims but something about the processing can’t be performed (either the key has changed so the token can’t be validated or if using a server side cache and the cache is empty). An end user isn’t going to be able to do much about this and they’re going to continue to get the error since the browser will keep sending the cookie.
Full article: http://brockallen.com/2012/10/22/dealing-with-session-token-exceptions-with-wif-in-asp-net/
In the same article he provides the following snippet of code that solved the issue in my case. In Global.asax:
void Application_OnError()
{
var ex = Context.Error;
if (ex is SecurityTokenException)
{
Context.ClearError();
if (FederatedAuthentication.SessionAuthenticationModule != null)
{
FederatedAuthentication.SessionAuthenticationModule.SignOut();
}
Response.Redirect("~/");
}
}
This problem caused by caching the SessionSecurityToken. The cache destination is in the local domain of application pool so when the .NET needs memory, it automatically will be wiped out. The best solution is two cancel the cacheing for security or implement your own subsystem for caching.
solution 1
AppFabric for Windows Server
memcached - a distributed memory object caching system
solution 2
var sessionSecurityToken = new SessionSecurityToken(principal, TimeSpan.FromHours(Convert.ToInt32(System.Web.Configuration.WebConfigurationManager.AppSettings["SessionSecurityTokenLifeTime"])))
{
IsPersistent = false, // Make persistent
IsReferenceMode = true // Cache on server
};
FederatedAuthentication.SessionAuthenticationModule.WriteSessionTokenToCookie(sessionSecurityToken);
We see this exact error if you browse to a freshly started application, while your browser still holds a cookie from an earlier session. Since these cookies are session cookies, the fix is to close all browser windows and browse again to the application.
Our application is a 'normal' web application redirecting to AD FS using WIF without any special security token cache things, as far as I know. However, we do use 'session mode' for WIF cookies (see for example "Your FedAuth Cookies on a Diet: IsSessionMode=true"), which makes the WIF cookies a lot smaller.
We are currently facing exactly the same problem, although our situation is a bit different. We are trying to use WIF to provide Shibboleth SSO for Outlook Web App (OWA). We have several OWA hosts behind a load balancer.
WIF generates the FedAuth cookie (and FedAuth1) which is more than 2.5 kB in size. Our load balancer truncates the cookie. So we set the IsSessionMode-Property to true in OWA's global.asax file. Now, the cookie size is reduced to approx. 600 bytes, which is fine. OWA works.
However, the Exchange Control Panel (ECP) which runs on the same server, does not work any longer. ECP runs in the same IIS application pool and has also the IsSessiobnMode-Property set in its global.asax file. Whenever ECP is called, the application does not send back any response but WIF reports:
Current user: 'User not set'
Request for URL 'http://owa.ourdomain.com/ecp/' failed with the following error:
System.IdentityModel.Tokens.SecurityTokenException: ID4243: Could not create a SecurityToken. A token was not found in the token cache and no cookie was found in the context.
This should be done if the number of claims in the token or the total size of the token is too big to be transfered back and forth in cookies. If that is not the case, then simplify your solution and just use the default setting that uses cookies. You shuold however, use certificate based cookie encryption, so it is still "farm friendly". By default WIF will enrcrypt using DPAPI that has machine affinity.
Related
We have a page with several forms. Each has its own #Html.AntiForgeryToken(). On my local machine everything is great.
We deployed to Azure (PAAS), but the __RequestVerificationToken is not being created on every request. Sometime it is there and sometime I get the The required anti-forgery cookie is not present and rarely I get the tokens do not match error.
I'm completely clueless at this point. I can't figure out if there's something wrong in our code or on Azure environment? No ajax in these forms.
We have added the <machineKey> section to our web.config. No caching. Sometimes it occurs on new devices from the first time.
After spending a significant amount of time with investigation, using a combination of Sentry and Azure web server logs, I've found 2 major causes of the mentioned errors:
1) On mobile phones, when the browser is in the background, it may be abruptly stopped by the OS to free up resources. When this happens, usually, the page is stored on the phone's drive, and reloaded from there once the browser is re-opened.
The problem, however, is that by this time, the Anti-Forgery Token, which is a session cookie, has already expired, since this is essentially a new session. So the page loads without an Anti-Forgery Cookie, using HTML from the previous session. This causes the The required anti-forgery cookie is not present exception.
2) While seemingly related, the tokens do not match exception is usually only tangentially related. The cause seems to be user behaviour of opening multiple tabs at the same time.
The Anti-Forgery Cookie is only assigned when a user arrives to a page with a form on it. This means that they can go to your homepage, and have no anti-forgery cookie. Then they can open multiple tabs using middle-click. The multiple tabs are multiple parallel requests, each of them without an anti-forgery cookie.
As these requests don't have an anti-forgery cookie, for each of them, ASP.NET generates a separate pseudo-random token for their cookie, and uses that in the form; however, only the result of the last header received will be retained. This means that all the other pages will have invalid tokens on the page, since their anti-forgery cookie was overridden.
For a solution, I've created a global filter that should ensure that
The Anti-Forgery cookie is assigned on any page, even if the page has no form, and
The Anti-Forgery cookie is not session-bound. It's lifetime should be adjusted to match the user login token, but it should persist between sessions in case a mobile device reloads the page without the session.
The code below is a FilterAttribute that has to be added inside FilterConfig.cs as a global filter. Please do note that, while I do not believe this would create a security hole, I am by no means a security expert, so any input is welcome.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AntiForgeryFilter : FilterAttribute, IActionFilter
{
public void OnActionExecuting(ActionExecutingContext filterContext)
{
var cookie = filterContext.HttpContext.Request.Cookies.Get(AntiForgeryConfig.CookieName);
var addCookie = true;
if (string.IsNullOrEmpty(cookie?.Value))
{
cookie = filterContext.HttpContext.Response.Cookies.Get(AntiForgeryConfig.CookieName);
addCookie = false;
}
if (string.IsNullOrEmpty(cookie?.Value))
{
AntiForgery.GetTokens(null, out string cookieToken, out string _);
cookie = new HttpCookie(AntiForgeryConfig.CookieName, cookieToken)
{
HttpOnly = true,
Secure = AntiForgeryConfig.RequireSsl
};
}
cookie.Expires = DateTime.UtcNow.AddYears(1);
if(addCookie) filterContext.HttpContext.Response.Cookies.Add(cookie);
}
public void OnActionExecuted(ActionExecutedContext filterContext)
{
}
}
I believe your problem comes from having multiple forms with different anti-forgery tokens on one page.
When page is requested you get two different tokens in forms hidden fields but only one token in the cookies.
Form POST contains mismatching tokens that causes an error.
Try how AFT will work for you if page contains only one form. If it works OK then my assumption is correct.
This answer contains possible solution for page with multiple forms however it has some security drawbacks as explained here.
I'm not sure why everything works OK on your localhost.
I've created simple application and tried form POST with correct cookies token but old Form token from previous session. To my surprise such POST successfully passes.
May be asp.net has some special handling for local requests in this case. I haven't found any info on this.
If my answer still doesn't help you could you please provide following data for further analysis:
Original page request with returned HTTP headers and form anti-forgery tokens.
Form POST request with sent HTTP headers.
We are getting random "The anti-forgery cookie token and form field token do not match" errors showing up in the error log in with our site hosted in Azure. After realizing that we needed a static machine key, we added to that to the web.config with the validationKey and decryptionKey attributes, however, we still have the random error popping up.
Just to define my use of "random" here, out of every ~200-300 form submissions, this happens once or twice. It just feels like too much to have happen and it's a real interruption to our clients that trust our services.
One other thought that crossed my mind is whether this is happening on machines that don't have cookies enabled. I haven't been able to verify that one way or another, but I didn't know if cookies are a requirement for the ValidateAntiForgeryToken to work. If it does require cookies, then should we pop up a message to our users letting them know that cookies are required for proper use?
I could use help coming up with ways to diagnose this or other ideas of how to handle this.
Thank you in advance.
[UPDATE] I just heard from a user where I saw this error popup. It turns out that they loaded the page and walked away for a while causing the error. That's great news since that means that the validation is just doing its job and nothing crazy was happening...I just need to verify if that data point is indicative of the rest of the users. So given that, how do you all handle situations where the token expires? Do you notify the user in some clean way?
Do you have a server farm ? i.e. do you have an App Service with auto-scaling enabled ? or a cloud service with several machines ?
If yes, check that all your machinekeys are defined with the same value in all your web.config files. The machinekey is used to generate AntiForgery tokens.
Could you log the headers of the failed requests ? You can add some code in your global.asax on Application_Error like :
foreach (string header in request.Headers)
{
// header <== header name
// request.Headers[header]) <== header value
}
Question #1:
Is setAuthCookie any less safe than FormsAuthentication.Encrypt(ticketVariable)?
I mean if anyone tries to modify the cookie created by setAuthCookie, by modifying the username, I suppose that'll violate the authentication on subsequent calls?
Question #2:
for those using iphones and tablets to access the site, I suppose FormsAuthentication will fail? Given that I don't want to use cookieless option, is there another approach to make the site secure on both smart phones web browsers and ummm none-smartphone web browsers?
cheers
SetAuthCookie basically creates a new FormsAuthenticationTicket with the supplied username & persistence options, serializes it, FormsAuthentication.Encrypt()'s it, and sets it in the Response.Cookies collection. SetAuthCookie and GetAuthCookie both call FormsAuthentication.Encrypt indirectly.
On subsequent requests, the FormsAuthentiationModule handles the AuthenticateRequest event. If it sees a cookie (it may have expired), it attempts to decrypt it's value with the machineKey (it may have been tampered with) and deserialize it back into a FormsAuthenticationTicket (it may be corrupt). If none of that (bad stuff) happens, the ticket contains the username, issue date, expiration info, etc.. If the ticket hasn't expired, an IIdentity and IPrincipal are created and assigned to HttpContext.Current.User and Thread.CurrentThread.Principal. In .NET 4.5 and later (I think), this is Claims-based (ClaimsIdentity, ClaimsPrincipal). Prior to that, it was a (GenericPrincipal, FormsIdentity) I think.
Any tampering at all on the user side will cause the request to be treated as anonymous. It will fail to decrypt. The only things that would compromise this validation would be if the machineKey in web.config/machine.config somehow got into the hands of an attacker or if there was a bug in the framework code (search for Padding Oracle for a historical example of this).
Aside from that, the other thing to watch out for would be session hijacking. If someone steals your cookie on a public wifi for example, they can present it to the server and the server will behave as if it's you. This generally involves network traffic sniffing. For these reasons, best practice is to use SSL for your entire site and set the cookie to HTTP only and Secure (only presented over https connections) in web.config/system.web/authorization/forms. HTTP only means that it will not be available to client-side Javascript. HTTP Only and Secure effectively means HTTPS only. This will only work if you use SSL on your entire site.
FormsAuthentication will work fine on mobile web browsers. It simply requires the client to accept cookies. As far as I know, all mobile devices will allow this.
We have an ASP.NET MVC 2 (.NET 4) application running on Windows Azure (latest 2.x OS version) with two web role instances.
We use the anti-forgery token supplied by MVC for all POST requests, and we have set a static Machine Key in web.config, so everything works on multiple machines and across restarts. 99.9% of the cases it works perfectly.
Every now and then, however, we log a HttpAntiForgeryException, with message "A required anti-forgery token was not supplied or was invalid."
I know the problem might be cookies not being allowed in the browser, but we've verified that and cookies are enabled and being sent back and forth correctly.
The error occurs with a variety of browsers and obviously causes problems to the users because they have to repeat the operation or they can lose some data. Suffice it to say, we haven't been able to reproduce the problem locally, but it only happens on Windows Azure.
Why is that happening? How can we avoid it?
I ran into this recently as well and found two causes.
1. Browser restores last session on open for page that is cached
If you have a page that is cachable that performs a post to your server (i.e. antiforgery will be on) and the user has their browser set to restore last session on start up (this option exists in chrome) the page will be rendered from cache. However, the request verification cookie will not be there because it is a browser session cookie and is discarded when browser is closed. Since the cookie is gone you get the anti-forgery exception. Solution: Return response headers so that the page is not cached (i.e. Cache-Control:private, no-store).
2. Race condition if opening more than one tab on start up to your site
Browsers have the option to open a set of tabs at start up. If more than one of these hit your site that returns a request verification cookie you can hit a race condition where the request verification cookie is overwritten. This happens because more than one request hits your server from a user that does not have the request verification cookie set. The first request is handled and sets the request verification cookie. Next the second request is handled, but it did not send the cookie (had not been set yet at request time) so the server generates a new one. The new one overwrites the first one and now that page will get an antiforgery request exception when it next performs a post. The MVC framework does not handle this scenario. This bug has been reported to the MVC team at Microsoft.
The anti forgery token contains the username of the currently connected user when it is emitted. And when verifying its validity, the currently connected user is checked against the one used when the token was emitted. So for example if you have a form in which the user is not yet authenticated and you emit an anti forgery token, there won't be any username stored in it. If when you submit the form you authenticate the user, then the token will no longer be valid. Same applies for logging out.
Here's how the Validate method looks like:
public void Validate(HttpContextBase context, string salt)
{
string antiForgeryTokenName = AntiForgeryData.GetAntiForgeryTokenName(null);
string str2 = AntiForgeryData.GetAntiForgeryTokenName(context.Request.ApplicationPath);
HttpCookie cookie = context.Request.Cookies[str2];
if ((cookie == null) || string.IsNullOrEmpty(cookie.Value))
{
throw CreateValidationException();
}
AntiForgeryData data = this.Serializer.Deserialize(cookie.Value);
string str3 = context.Request.Form[antiForgeryTokenName];
if (string.IsNullOrEmpty(str3))
{
throw CreateValidationException();
}
AntiForgeryData data2 = this.Serializer.Deserialize(str3);
if (!string.Equals(data.Value, data2.Value, StringComparison.Ordinal))
{
throw CreateValidationException();
}
string username = AntiForgeryData.GetUsername(context.User);
if (!string.Equals(data2.Username, username, StringComparison.OrdinalIgnoreCase))
{
throw CreateValidationException();
}
if (!string.Equals(salt ?? string.Empty, data2.Salt, StringComparison.Ordinal))
{
throw CreateValidationException();
}
}
One possible way to debug this is to recompile ASP.NET MVC from its source code and log exactly in which of the if cases you enter when the exception is thrown.
I have a few MVC3 web apps that get this pretty regularly also. The majority of them are because the client doesn't send a POST body. And most of these are IE8 because of some bug with ajax requests preceding a regular form post. There's a hotfix for IE that seems to address the symptoms, which sort of proves that it is a client bug in these cases
http://support.microsoft.com/?kbid=831167
There are a few discussions about the issue around the web, nothing too useful though, I definitely am not about to mess with keep-alive timeouts which is a suggested "solution" in some places...
https://www.google.com/search?q=ie8+empty+post+body
I've never been able to reproduce it with a variety of attempts to reset connections between POSTS so I'm afraid I don't have a real solution for the case of the IE empty POST bodies. The way we've mitigated it a little bit is to make sure that we never use the POST method when just retrieving data via ajax.
If you log the full request, check to see if the POST body is empty, and if it is, it'll probably be an older IE. And I don't mean Content-Length: 0, it will usually have a Content-Length that seems correct in the headers but there will literally be nothing after the headers in the request.
The issue as a whole is still a mystery to me though because we still get the occasional exception where there is a complete POST body. Our usernames never change and our keys are static as well, I haven't tried adding debugging to the source, if I ever get around to that I will report my findings.
There are a couple of options for what you could try. You could try remoting into the machine and looking at the event log to see if you can get more information from that in regards to where this is happening. If that doesn't help, you can use DebugDiag or some other tool to capture a dump of the process (DebugDiag will let you capture one at the time of this specific exception). And then look at that to see what is going on.
If you can't seem to figure it out from there, you can always create a support case with Microsoft to help you investigate it.
I have encountered similar problems with my home-brewed anti-forgery code, which is conceptually very similar to the MVC mechanism. Mostly the problem seems to occur because modern browsers appear willing to display cached copies of pages specified as non-cached.
I have tried all combinations of page no-cache directives, but sometimes I still get cached pages displayed.
I have found that a better solution is to hook the onbeforeunload event for the page and explicitly clear the value of the hidden input field holding the token value in the DOM.
If a cached copy of a page is loaded, it seems to contain the cleared input field value. I then test for this in the document ready function and reload the page if necessary:
window.location.reload(true);
Seems to work quite effectively, and I suspect it might for the MVC anti-forgery code too.
I'm using ELMAH to handle errors in my MVC sites and I've noticed over the past couple of weeks that I'm getting some CryptographicExceptions thrown. The message is:
System.Security.Cryptography.CryptographicException: Padding is invalid and cannot be removed.
System.Web.Mvc.HttpAntiForgeryException:
A required anti-forgery token was not
supplied or was invalid. --->
System.Web.HttpException: Validation
of viewstate MAC failed. If this
application is hosted by a Web Farm or
cluster, ensure that
configuration specifies the same
validationKey and validation
algorithm. AutoGenerate cannot be used
in a cluster. --->
The application is not running in a cluster and I can't seem to reproduce these errors. They look like valid requests -- not a hand-crafted post -- and do contain the __RequestVerificationToken cookie. I do have the required HTML helper on the page, inside the form (my login form).
I haven't had any user complaints, yet, so I'm assuming that eventually it works for whoever is trying to login, but I'm left wondering why this could be happening.
Anyone else seeing this behavior or have any ideas on how to diagnose the exception -- like I said, I can't get it to fail. Deleting the cookie in FF comes up with a different error. Modifying the cookie (changing or removing the contents) also results in a different error, as does modifying the contents of the hidden token input on the page.
I'm not sure if there is a correlation, but after adding a robots.txt file that excludes my login actions, I am no longer seeing these errors. I suspect that it has to do with a crawler hitting the page and trying to invoke the login action.
EDIT: I've also see this issue when receiving old cookies after the application pool has recycled. I've resorted to setting the machineKey explicitly so that changes to the validation/decryption keys on application restarts don't affect old cookies that may be resent.
After updating the site and going to a fixed machineKey I found that I was still getting these errors from people who had cookies from the previous version. As a temporary work around I've added the following Application_Error handler:
public void Application_Error()
{
var exception = Server.GetLastError().GetBaseException();
if (exception is System.Security.Cryptography.CryptographicException)
{
Server.ClearError();
if (Request.IsAuthenticated)
{
var form = new FormsAuthenticationWrapper();
form.SignOut();
Session.Clear();
}
Response.Cookies.Clear();
Response.Redirect( "~" );
}
}
I'm not so sure this has anything specifically to do with the antiforgery system, the inner exception states 'Validation of viewstate MAC failed.', from what I can tell, the default infrastructure for the antiforgery system has a dependency on the viewstate (actually if you take a look here you'll see see the dependency and horror (the CreateFormatterGenerator method at the bottom)).
As for why the viewstate mac is failing on the fake request, I'm not sure- but given the horror that exists in deserializing the antiforgery token (processing an entire fake request), it doesn't suprise me at all..