I'm trying to find out why UrlHelper.RouteUrl returns me cookieless URLs that start with /(F(. This only seems to happen for Bing requests (Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)).
I already disabled cookieless mode 3 times:
<authentication mode="None">
<forms cookieless="UseCookies" />
</authentication>
<anonymousIdentification enabled="true" cookieless="UseCookies" />
<sessionState cookieless="UseCookies" />
I also added the following assertion:
if (url.StartsWith("/(F(", StringComparison.Ordinal))
throw new Exception(
FormsAuthentication.CookieMode + " " +
FormsAuthentication.CookiesSupported + " " +
HttpContext.Current.Request.Browser.Cookies);
This throws in case of bing bot. But it claims that CookieMode == UseCookies && CookiesSupported == true && Browser.Cookies == true. This means that the config setting took, as well as that ASP.NET thinks that Bing bot does support cookies. There should be no reason whatsoever to prepend this cookieless string to the URL.
I cannot reproduce it locally on Windows 7 .NET 4.7. The production server runs Server 2008 R2 with .NET 4.7.
I tried really hard disabling this nasty feature. How can I escape this madness?
Update: The F seems to mean that the forms authentication feature is responsible. But clearly it is disabled in the web.config?! I'm not using it in any way as far as I know (might be a wrong assumption).
Also, I tested the "app path modifier" value which is being used by MVC:
var x =
(string)typeof(HttpResponse)
.GetField("_appPathModifier", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy)
.GetValue(HttpContext.Current.Response);
I added this value to the assert and indeed the nasty /(F( string is present here. I have no idea how it comes to be that the .NET Framework sets this value.
Because before that, the value of SessionId has been removed (CookielessHelper.RemoveCookielessValuesFromPath) from url path.
HttpContext's Init method process that.
private void Init(HttpRequest request, HttpResponse response)
{
this._request = request;
this._response = response;
this._utcTimestamp = DateTime.UtcNow;
this._principalContainer = this;
if (this._wr is IIS7WorkerRequest)
{
this._isIntegratedPipeline = true;
}
if (!(this._wr is StateHttpWorkerRequest))
{
this.CookielessHelper.RemoveCookielessValuesFromPath();
}
// other codes ...
}
Please Ref Possible Bug With ASP.NET MVC 3 Routing?
I will check Response._appPathModifier value at Application_BeginRequest event,
protected void Application_BeginRequest(object sender, EventArgs e)
{
//到這時,Url已被改成沒包含SessionId
//但它的值會放在 Response._appPathModifier 變數之中
var appPathModifierFieldInfo = Context.Response.GetType().GetField("_appPathModifier",
BindingFlags.NonPublic | BindingFlags.Instance);
var appPathModifier = appPathModifierFieldInfo.GetValue(Context.Response);
if (appPathModifier != null)
{
//url 中有 SessionId
throw new HttpException(404, "Not found");
}
}
Related
ASP .NET 4.0 with MVC 3.0
So here's the situation: I am new to MVC and ASP.NET and I have a MVC application that uses FormAuthentication with the following in the web.config:
<authentication mode="Forms">
<forms loginUrl="~/LogOn/LogOn" timeout="2" requireSSL="true" />
</authentication>
And what usually happens is that if a user navigates to a page after the session has expired, they are directed to the LogOn page, which works fine. What I am trying to do is prevent that page from being sent back if the request is an Ajax call and instead send an JSON object back to indicate failure.
I've gotten as far as catching the request, checking for the XMLHttp type via the below:
void MvcApplication_AuthenticateRequest(object sender, EventArgs e)
{
if (s_identityProvider == null)
return;
IClaimsPrincipal principal = GetClaimsPrincipal();
Context.User = principal;
if (principal != null)
ClaimProvider.CheckAndPopulateRoles(principal);
else
{
if (Context.Request.Headers["X-Requested-With"] == "XMLHttpRequest")
{
Context.Response.Write( "<?xml version='1.0' encoding='ISO-8859-1'?>"+
"<note>"+
"</note>";
}
else
{
FormsAuthentication.SignOut();
}
}
}
Now, I've tested that my check for AJAX call seems to work, the only thing that is bizarre is that after the catch, the login page is STILL sent as a response. I have checked for all uses of FormsAuthentication methods to make sure no one else is forcing a FormsAuthentication.RedirectToLoginPage() and checked, in fact, all uses of FormsAuthentication and none are doing anything bizarre. I've also checked for the query for the login page URL (via GetRedirectUrl() ) and still nothing.
Does anyone have any ideas what would be doing the autoredirect?
My only guess would be that the AuthenticateRequest method is not the end of the Request's life cycle here. As a result, the Response that is ultimately returned to the user is the redirect response. You should try explicitly ending the Response after you have caught and modified the AJAX response to avoid any further manipulation by the MVC framework
if (Context.Request.Headers["X-Requested-With"] == "XMLHttpRequest")
{
Context.Response.Write( "<?xml version='1.0' encoding='ISO-8859-1'?>"+
"<note>"+
"</note>";
Context.Response.End(); // Calling End here should complete the response.
}
else {
// ... other stuff
}
I have two sites, both on the same domain, but with different sub-domains.
site1.mydomain.example
site2.mydomain.example
Once I'm authenticated on each, I look at the cookies included in subsequent request and they are identical for each site.
However, if I log into the first site, and then navigate to the other, I expect my cookie from site 1 to be sent with the request to site2, but this is not the case. Here are the properties of my cookies.
Logging into Site1, this cookie then exists
Name = MySite
Domain =
Has Keys = False
HttpOnly = False
Path = /
Value = 1C41854066B03D8CC5679EA92DE1EF427DAC65D1BA0E672899E27C57245C1F0B7E93AB01B5563363AB4815A8F4BDE9D293FD261E03F8E60B8497ABBA964D8D315CCE1C8DD220C7176E21DC361935CF6
Expires = 1/1/0001 12:00:00 AM
Logging into Site2, these cookies then exists.
Name = MySite
Domain =
Has Keys = False
HttpOnly = False
Path = /
Value = C8C69F87F993166C4D044D33F21ED96463D5E4EB41E1D986BF508DA0CBD5C2CA7D782F59F3BC96871108997E899FF7401C0D8615705BDB353B56C7E164D2302EE6731F41705016105AD99F4E0578ECD2
Expires = 1/1/0001 12:00:00 AM
I've set the domain on each (doesn't show up in a request cookie as it's only needed on the client).
I've made sure my Forms setting for each are identical
I've made sure my machine key settings are the same in both web configs.
I'm at a loss on why this isn't working. What is it that a cookie contains that the client will send it for one sub-domain and not the other when they are both using the same auth cookies so far as I can tell?
Please comment if there is more info you'd like to see. I've been struggling with this for two days now. According to this article this should be working.
code added
Here is my config file setting for my authentication. This is used in both sites.
<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn"
defaultUrl="~/Home/Index"
name="MySite"
protection="All"
path="/"
domain="mydomain.example"
enableCrossAppRedirects="true"
timeout="2880"
/>
And here is my code to create the cookie in Site1.
//Add a cookie that the Site2 will use for Authentication
var cookie = FormsAuthentication.GetAuthCookie(userName, true);
cookie.Name = "MySite";
cookie.HttpOnly = false;
cookie.Expires = DateTime.Now.AddHours(24);
cookie.Domain = "mydomain.example";
HttpContext.Response.Cookies.Add(cookie);
HttpContext.Response.Redirect(site2Url,true);
UPDATE 2:
I noticed something strange while testing. When I add a cookie to the response for site1, it get's added to this directory...
C:\Users\jreddy\AppData\Roaming\Microsoft\Windows\Cookies
When I add a cookie to the response for site, it gets added to this directory...
C:\Users\jreddy\AppData\Roaming\Microsoft\Windows\Cookies\Low
That could be my problem. Could it be that one of my sites is included in the local intranet zone?
UPDATE 3: Problem found, solution unknown
It seems that my problem has to do with my second site being part of the Local Intranet Zone. If I go to Site1 using Firefox it works, but I have to enter my Windows credentials. If I go thru IE, my credentials are picked up automatically, but the cookies can't be read by site2. I may ask this in another question.
Set the property of Domain to .mydomain.example in each Cookies of two subdomains websites. Like:
Response.Cookies["test"].Value = "some value";
Response.Cookies["test"].Domain = ".mysite.example";
In Site A:
HttpCookie hc = new HttpCookie("strName", "value");
hc.Domain = ".mydomain.example"; // must start with "."
hc.Expires = DateTime.Now.AddMonths(3);
HttpContext.Current.Response.Cookies.Add(hc);
In Site B:
HttpContext.Current.Request.Cookies["strName"].Value
Add new cookie and specify domain like this
HttpCookie cookie = new HttpCookie("cookiename", "value");
cookie.Domain = "domain.example";
For forms authentication set this in web.config
<forms name=".ASPXAUTH"
loginUrl="login.aspx"
protection="All"
timeout="30"
path="/"
requireSSL="false"
domain="domain.example">
</forms>
The cookie will be accessible to all the subdomains.
In order for each domain to decrypt the the cookie, all web.config files must use the same encryption/decryption algorithm and key. (how to create a machine key)
Example:
// do not wrap these values like this in the web.config
// only wrapping for code visibility on SO
<machineKey
validationKey="21F090935F6E49C2C797F69BBAAD8402ABD2EE0B667A8B44EA7DD4374267A75
D7AD972A119482D15A4127461DB1DC347C1A63AE5F1CCFAACFF1B72A7F0A281
B"
decryptionKey="ABAA84D7EC4BB56D75D217CECFFB9628809BDB8BF91CFCD64568A145BE59719
F"
validation="SHA1"
decryption="AES"
/>
For easier deployments, these values can be stored in a separate file:
<machineKey configSource="machinekey.config"/>
For added security you can also encrypt the machine key for further protection..
If you're using Forms authentication on all of your sub domains, all you need to do is to add domain=".mydomain.example" property to the <forms> node in your web.config
Note the leading period in .mydomain.example
This simple change by itself will make your authentication cookie valid in all sub-domains; no need to manually set any cookies.
I've created a HttpContext extension method that will write a sub domain safe cookie.
Blog post and discussion
public static class HttpContextBaseExtenstions
{
public static void SetSubdomainSafeCookie(this HttpContextBase context, string name, string value)
{
var domain = String.Empty;
if (context.Request.IsLocal)
{
var domainSegments = context.Request.Url.Host.Split('.');
domain = "." + String.Join(".", domainSegments.Skip(1));
}
else
{
domain = context.Request.Url.Host;
}
var cookie = new HttpCookie(name, value)
{
Domain = domain
};
context.Response.SetCookie(cookie);
}
}
// usage
public class MyController : Controller
{
public ActionResult Index()
{
this.Context.SetSubdomainSafeCookie("id", Guid.NewGuid().ToString());
return View();
}
}
I have a site that works as expected on my development box. That is, the formsauthentication ticket expires after 30 days. This is achieved through the following code
string roles = UserManager.getAuthenticationRoleString(txtUsername.Text);
HttpCookie formscookie = FormsAuthentication.GetAuthCookie(txtUsername.Text, true);
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(formscookie.Value);
FormsAuthenticationTicket newticket = new FormsAuthenticationTicket(1, ticket.Name, DateTime.Now, DateTime.Now.AddDays(30), true, roles, ticket.CookiePath);
HttpCookie newCookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(newticket));
newCookie.Expires = DateTime.Now.AddDays(30);
Response.Cookies.Add(newCookie);
I used fiddler to check that the expiration is set properly and I get this
.ASPXAUTH=84AB5430CF4B1C5F9B59C9285288B41F156FCAFA2A169EACE17A7778A392FA69F66770FD8A08FFD06064B00F0BD788FEEC4A5894B7089239D6288027170A642B3B7EB7DB4806F2EBBCF2A82EE20FD944A38D2FE253B9D3FD7EFA178307464AAB4BCB35181CD82F6697D5267DB3B62BAD; expires=Thu, 21-Jan-2010 18:33:20 GMT; path=/; HttpOnly
So I would expect it to expire in 30 days...But it only makes it about 30 minutes.
I have 3 other interesting tidbits about my environment / code
On the production box there are two sites pointing at the same code one for external access and one for internal access
When the I do get the login page because of premature expiration, the .ASPAUTH cookie is still there and sent to the browser
There is some role checking in the global.asax that looks like this
-
protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
if (HttpContext.Current.User != null)
{
if (HttpContext.Current.User.Identity.IsAuthenticated)
{
if (HttpContext.Current.User.Identity is FormsIdentity)
{
FormsIdentity id = (FormsIdentity)HttpContext.Current.User.Identity;
FormsAuthenticationTicket ticket = id.Ticket;
// Get the stored user-data, in this case, our roles
string userData = ticket.UserData;
string[] roles = userData.Split('|');
HttpContext.Current.User = new System.Security.Principal.GenericPrincipal(id, roles);
}
}
}
}
You'll need to add a machine key tag to web.config file. It's getting regenerated and that causes your premature timeout.
This is similar to the following question:
figuring out why asp.net authentication ticket is expiring
If the problem is that the user on the production box is kicked and has to log in again via FormsAuthentication, it's possible that the problem is IIS related and not .NET.
I've run into issues before where all the timeout settings in the world within the app didn't make a difference and the user was booted far too early.
Check your IIS settings for both the website and the application pool. There are settings in both related to timeouts, etc.
If II6:
Under website properties -> Home Directory tab -> Configuration Button -> Options Tab -> there is session state/length info here
Under application pool for your site -> Performance and Health tabs -> both have several settings that may recycle your pool (and essentially force a re-logon)
To debug, you could disable all health and performance checks on the pool, however be very careful as this could throttle your server if the app gets out of control.
You could also try putting the timeout settings in the web.config:
<system.web>
<authentication mode="Forms">
<forms timeout="XXXXX"/>
</authentication>
</system.web>
Anyway just some ideas from personal experience of similar issues. Hope it might help!
I've already written an HTTPHandler that gets POSTed from a ColdFusion page and it works successfully; now, I am trying to write a web application in ASP.NET so I can post a form to the .ashx handler from an .aspx page.
Application Trace (trace.axd) shows the following as my last 3 entries:
2 8/14/2009 1:53:56 PM /Default.aspx 200 GET View Details
3 8/14/2009 1:54:04 PM /Default.aspx 200 POST View Details
4 8/14/2009 1:54:13 PM /UploadHandler.ashx 401 POST View Details
I have a breakpoint in my .ashx file but it is never reached (I guess because of the 401 status code). Here is the snippet of code from the default.aspx trying to POST to the handler:
protected void UploadHandlerButton_Click(object sender, EventArgs e)
{
if (FileUpload1.HasFile)
{
try
{
ASCIIEncoding encoding = new ASCIIEncoding();
byte[] data = encoding.GetBytes(BuildFormData());
string baseAddress = "http://" + Environment.MachineName;
string pathInfo = Page.ResolveUrl("UploadHandler.ashx");
string URI = baseAddress + pathInfo;
HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(URI);
myRequest.Method = "POST";
myRequest.ContentType = "application/x-www-form-urlencoded";
myRequest.ContentLength = data.Length;
Stream newStream = myRequest.GetRequestStream();
newStream.Write(data, 0, data.Length);
newStream.Close();
}
catch (Exception someError)
{
LogText("FAILURE: " + someError.Message);
}
}
}
Here is a snippet of code from the UploadHandler.ashx file (but this doesn't appear to be reached):
public void ProcessRequest(HttpContext context)
{
string returnURL = context.Request.ServerVariables["HTTP_REFERER"];
string message;
message = UploadFile(context);
StringBuilder msgReturn = new StringBuilder(returnURL);
msgReturn.Append("?n=");
msgReturn.Append(HttpUtility.UrlEncode(TRIMrecNumAssigned));
msgReturn.Append("&m=");
msgReturn.Append(HttpUtility.UrlEncode(message));
context.Response.Redirect(msgReturn.ToString());
}
Both default.aspx and UploadHandler.ashx are in the root of a virtual directory on my localhost; the directory security is currently set to "Anonymous access" CHECKED and "Integrated Windows authentication" CHECKED.
When I click the "View Details" link on the trace.axd display, I see all the data in the Forms collection that I expect to see and hope to process but this 401 seems to be stopping everything. I could post the code for my little function called BuildFormData() if useful.
EDIT: Revised handler as follows (has had no effect; same error occurs):
public void ProcessRequest(HttpContext context)
{
//-----------------------------------------------------------------------------------------
// the remainder of this block is alternative to the .Redirect and is useful for debugging.
context.Response.ContentType = "text/html";
//context.Response.Write(TRIMrecNumAssigned);
//context.Response.Write("<p>");
//context.Response.Write(msgReturn);
context.Response.Write("<H1>Trim - Kerberos Prototype for ColdFusion consuming pages</h1>");
HttpContext.Current.Trace.IsEnabled = true;
HttpContext.Current.Trace.Write(null);
HttpContext.Current.Trace.Write("-------");
HttpContext.Current.Trace.Write(context.Request.Form["txtTrimRecordType"]);
HttpContext.Current.Trace.Write(GetUserInfo());
HttpContext.Current.Trace.Write("-------");
HttpContext.Current.Trace.Write(null);
using (Html32TextWriter htw = new Html32TextWriter(context.Response.Output))
{
typeof(TraceContext)
.GetMethod("Render", BindingFlags.NonPublic | BindingFlags.Instance)
.Invoke(HttpContext.Current.Trace, new object[] { htw });
}
}
Have you tried turning off Integrated Windows Auth and just leaving anonymous checked? Does it make a difference?
Your answer: "I think it made things worse because now I cannot even browse to default.aspx. I get this: HTTP 401.3 - Access denied by ACL on resource Internet Information Services"
My response: This is actually a good thing. This means we're getting closer to what is going on. If you're getting that error message and the only thing you have enabled is anonymous authentication via IIS, that means that the ASP.NET impersonation user doesn't have NTFS permissions on the files in question.
I'm not sure if you are on XP or Win 2k3, but now you want to check and make sure that either the ASPNET (XP) or Network Service (Win 2k3) users have at least read access on the files in question. Make sure that user has at least that level of access and then let me know how it goes.
Update: I don't know why I didn't think of this before. You may need to set credentials on your HttpWebRequest. To use the credentials of the current user, try adding this to your request.
HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(URI);
myRequest.Credentials = CredentialCache.DefaultCredentials;
If you need to add different credentials you can try Network Credentials
There's a good explanation of credentials here.
Hope this helps.
Looking at your ProcessRequest(), you do the following:
string returnURL = context.Request.ServerVariables["HTTP_REFERER"];
Based on how you are calling it with HttpWebRequest, this variable will be null. Then when you create your msgReturn, it will look something like this:
?n=XXX%m=YYY
When you redirect to this URL, it will probably not be found which is what is returning the 401.
I have a blank test app created in VS 2005 as ASP.NET application. MSDN says that
By default, ASP.NET does not use impersonation, and your code runs using the ASP.NET application's process identity.
And I have the following web.config
<configuration>
<appSettings/>
<connectionStrings/>
<system.web>
<!--
Set compilation debug="true" to insert debugging
symbols into the compiled page. Because this
affects performance, set this value to true only
during development.
-->
<compilation debug="true" defaultLanguage="c#" />
<!--
The <authentication> section enables configuration
of the security authentication mode used by
ASP.NET to identify an incoming user.
-->
<authentication mode="Windows"/>
<identity impersonate="false"/>
<!--
The <customErrors> section enables configuration
of what to do if/when an unhandled error occurs
during the execution of a request. Specifically,
it enables developers to configure html error pages
to be displayed in place of a error stack trace.
<customErrors mode="RemoteOnly" defaultRedirect="GenericErrorPage.htm">
<error statusCode="403" redirect="NoAccess.htm" />
<error statusCode="404" redirect="FileNotFound.htm" />
</customErrors>
-->
</system.web>
</configuration>
So it seem impersonation is disabled just like the article is suggesting.
My aspx is blank default and the codebehind is
namespace TestWebapp
{
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
System.Diagnostics.Debug.WriteLine(String.Format("Before1: Current Princupal = {0}", Thread.CurrentPrincipal.Identity.Name));
WindowsImpersonationContext ctx = WindowsIdentity.Impersonate(IntPtr.Zero);
try
{
int a = 0;
System.Diagnostics.Debug.WriteLine(String.Format("After: Current Princupal = {0}", Thread.CurrentPrincipal.Identity.Name));
} finally
{
ctx.Undo();
}
}
}
}
When I reload the page I get the following debug output:
[5288] Before1: Current Princupal =
DOMAIN\User
[5288] After: Current Princupal =
DOMAIN\User
Output is the same with
<identity impersonate="false"/>
The web site uses Default Application Pool and the pool is set up to use NETWORK SERVICE account for its worker processes.
I'm sure the application uses the web.config it should use and the w3p.exe worker process is running under NETWORK SERVICE.
What can be wrong in this case?
Thanks!
#Edit: Rob, thanks for the tip!
The $user shortcut shows me that everything is happening as I expect: with impersonation on I have the process running user NT AUTHORITY\NETWORK SERVICE and the thread has DOMAIN\User before WindowsIdentity.Impersonate(IntPtr.Zero) and "No Token. Thread not impersonating." after.
But Thread.CurrentPrincipal.Identity.Name and HttpContext.Current.User.Identity.Name still give me DOMAIN\User in both places.
#Edit: I've found out that to get Thread.CurrentPrincipal and HttpContext.Current.User changed I have to manually do it:
Thread.CurrentPrincipal = new WindowsPrincipal(WindowsIdentity.GetCurrent());
HttpContext.Current.User = Thread.CurrentPrincipal;
I'm not sure what's the point here, but anyway. I now have a problem with sharepoint shared services manage user profile permission but that's another question.
I think I understand your problem here.
Things to know before moving further,
There are different security context while an application is running. Like System.Security.Principal.WindowsIdentity.GetCurrent().Name, and the one you mentioned above, i.e. System.Threading.Thread.CurrentPrincipal.Identity.Name
In a web application, System.Threading.Thread.CurrentPrincipal.Identity is always provided by HttpContext.Current.User.Identity.
Coming to your point. If you want to modify System.Threading.Thread.CurrentPrincipal.Identity, then modify HttpContext.Current.User.Identity which initially provided by your authentication mechanism.
Seems odd, A few things to try:
While in on a breakpoint in Debug type $user in a watch window, that will show you the process and thread identities.
Your use of impersonate is incorrect, try this code:
// Declare the logon types as constants
const long LOGON32_LOGON_INTERACTIVE = 2;
const long LOGON32_LOGON_NETWORK = 3;
// Declare the logon providers as constants
const long LOGON32_PROVIDER_DEFAULT = 0;
const long LOGON32_PROVIDER_WINNT50 = 3;
const long LOGON32_PROVIDER_WINNT40 = 2;
const long LOGON32_PROVIDER_WINNT35 = 1;
[DllImport("advapi32.dll", EntryPoint = "LogonUser")]
private static extern bool LogonUser(
string lpszUsername,
string lpszDomain,
string lpszPassword,
int dwLogonType,
int dwLogonProvider,
ref IntPtr phToken);
public static WindowsImpersonationContext ImpersonateCurrentUserBegin(System.Net.NetworkCredential credential)
{
WindowsImpersonationContext impersonationContext = null;
if (credential == null || credential.UserName.Length == 0 || credential.Password.Length == 0 || credential.Domain.Length == 0)
{
throw new Exception("Incomplete user credentials specified");
}
impersonationContext = Security.Impersonate(credential);
if (impersonationContext == null)
{
return null;
}
else
{
return impersonationContext;
}
}
public static void ImpersonateCurrentUserEnd(WindowsImpersonationContext impersonationContext)
{
if (impersonationContext != null)
{
impersonationContext.Undo();
}
}
What does HttpContext.User.Identity.Name give you?
Assume you've checked the security tab within IIS that it allows anonymous access?
Are you within an active directory that has some strange local policy?