ASP.NET cross subdomain cookies not being attached to request - asp.net

I've been trying to build a local proof of concept to move over our current solution to a different URL structure. I'm using ASP.NET with 3 projects that currently has these URLs mapped to them:
mysite.com
mysite.com/api
mysite.com/app
To setup the proof of concept I've setup 3 sites locally in IIS with the following URLs:
mysite.com
api.mysite.com
app.mysite.com
And have added the following entries into the HOSTS file:
127.0.0.1 mysite.com
127.0.0.1 app.mysite.com
127.0.0.1 api.mysite.com
Currently app.mysite.com talks to api.mysite.com to perform a user login, which returns a cookie back in the response. The issue is that the cookie is not being stored under mysite.com. Subsequent requests to api.mysite.com don't have the cookie attached in the request header, and therefore fail.
I've experimented setting the cookie's domain property with no success, as well as not including a domain property.
An example of a cookie returned in the request:
Set-Cookie: MyCookie=somestuff; domain=.mysite.com; expires=Sat, 06-Sep-2014 00:02:04 GMT; path=/; HttpOnly
Yet the cookie is never attached to any requests to api.mysite.com nor can i see it in the cookie browser of Chrome, Firefox, IE etc...
Note that I've enabled CORS in web.config to enable cross domain requests.
EDIT:
In response to Owain's answer. I'll clarify my current setup a little more.
Regarding <machineKey> I have created a machineKey and used the same values on both applications in the web.config file. This was already working locally and in production when using mysite.com/api and mysite.com/app It wasn't till moving to subdomains that i ran into this issue.
Here is my code for creating and attaching the cookie:
private void EncryptAndAttachCookieToHeaders(FormsAuthenticationTicket ticket)
{
string encryptedTicket = FormsAuthentication.Encrypt(ticket);
HttpCookie newCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
newCookie.Domain = ".mysite.com";
newCookie.Expires = DateTime.Now.AddMonths(3);
newCookie.HttpOnly = true;
newCookie.Secure = false;
System.Web.HttpContext.Current.Response.Cookies.Add(newCookie);
// For testing purposes
HttpCookie hc = new HttpCookie("cookie1", "value");
hc.Domain = ".mysite.com";
hc.Expires = DateTime.Now.AddMonths(3);
HttpContext.Current.Response.Cookies.Add(hc);
HttpCookie hd = new HttpCookie("cookie2", "value");
hd.Domain = ".api.mysite.com";
hd.Expires = DateTime.Now.AddMonths(3);
HttpContext.Current.Response.Cookies.Add(hd);
All of these cookies (real one plus the two tests) are visible when viewing the response in Fiddler. However subsequent requests to api.mysite.com do NOT have any cookies attached in the request header. The browser doesn't seem to want to store the cookie now that I've moved to the subdomain structure.

To make cross domain login work you first need to edit the <machineKey> element inside the web.config.
By default it is something like this
<machineKey
validationKey="AutoGenerate,IsolateApps"
decryptionKey="AutoGenerate,IsolateApps"
validation="SHA1"
decryption="Auto" />
With these settings, the .NET framework uses the automatically generated validationKey and decrytionKey
With the default settings the validation key and the decryption key will get generated by ASP.NET and will get used for authentication ticket and cookie, if you want multiple applications to share the same authentication ticket and cookie, you just need to set the validationKey and decrytionKey in all the applications to the same values
Make sure they are the same in both application something like for example
<machineKey
validationKey="FBFF3D4EFD359FD58AA480E0A63A9C817463A30EF5AFF4E212AD3321C122AECFFEC427C26D24B67296F5EBBB6A3736BF37A5027718E5426B92C9AC606F9AD66F"
decryptionKey="048F8A9F3D6A7D2E88738B36BEEB85FF0B4E23EEF11976D1C0F6F03B91CCFC37"
validation="SHA1"
decryption="AES" />
You can easily create some keys on this site http://aspnetresources.com/tools/machineKey
To make the authentication cookie visible at all other sub domains, you'll need to modify the cookie’s domain attribute.
// Call SetAuthCookie method
FormsAuthentication.SetAuthCookie("Test account", false);
//modify the Domain attribute
System.Web.HttpCookie TestCookie =
System.Web.Security.FormsAuthentication.GetAuthCookie(User.Identity.Name.ToString(), false);
TestCookie.Domain = "mysite.com"; // (Also try ".mysite.com" i'm unsure about this one) the second level domain name
Response.AppendCookie(TestCookie);
This should be everything you need to make cross domain cookies work

I found out the answer. The issue actually resided in how the AJAX request was being formed. I had to add an AJAX property called withCredentials to allow the browser to send the cross domain cookie. A drawback to this approach is that IE 9 and older don't support it....
From post Setting a cookie on a subdomain from an ajax request :
Set the allow Credentials header on api
Access-Control-Allow-Credentials: true
Use withCredentials for the request
$.ajax({
url: a_cross_domain_url,
xhrFields: {
withCredentials: true
}
});
Otherwise the XMLHttpRequest will not send the cookies, regardless of the Access-Control-Allow-Credentials header.
Remove the wildcard on Access-Control-Allow-Origin
Access-Control-Allow-Origin: http://www.example.com
The wildcard * will not work. The browser will discard the response if withCredentials was set.

Related

ASP.NET_SessionId not found in OWIN OpenIdConnectAuthentication

BackGround : User once logged in to the our Web Application (using App level credential) will be presented with with Mail System they want to use based on that user will be redirected to respective authorization server to authenticate (using login / password of their mail system) and the auth server will return an access token back.
In Notification events like OnAuthorizationCodeReceivedAsync or OnAuthenticationFailedAsync; we are not getting ASP.NET_SessionId so having said that i am not able to use any of the session values which are set before OAuth Flow.
Refer below Code for more details.
app.UseOpenIdConnectAuthentication(New OpenIdConnectAuthenticationOptions With {
.ClientId = appId,
.ClientSecret = appSecret,
.Authority = "https://login.microsoftonline.com/common/v2.0",
.Scope = $"openid email profile offline_access {ewsScopes}",
.RedirectUri = redirectUri,
.PostLogoutRedirectUri = redirectUri,
.TokenValidationParameters = New TokenValidationParameters With {
.ValidateIssuer = False
},
.Notifications = New OpenIdConnectAuthenticationNotifications With {
.AuthenticationFailed = AddressOf OnAuthenticationFailedAsync,
.AuthorizationCodeReceived = AddressOf OnAuthorizationCodeReceivedAsync
}
})
I am not able to get any session values in HttpConext.Current.Session which are set before OAuth flow in notification events.
As per below SO; i tried different approaches like SystemWebCookieManager, UseKentorOwinCookieSaver but issue not resolved.
ASP.NET_SessionId + OWIN Cookies do not send to browser
What could be the issue and how can I resolve it?
ByDefault; OpenIDConnect uses form post redirects which are incompatible with SameSite. Due to that Application Session cookie not sent over and that is how it should be.
As per couple of Stack overflow link below; using either URL rewrite or below web.config allows us to maintain session when response is posted back to Callback url but we still need to use Owin's SystemWebCookieManager for that in order to work.
Browser won't set ASP.NET_SessionId cookie on payment gateway's post request to our site
how SameSite attribute added to my Asp.net_SessionID cookie automatically?
Considering above scenario; for OpenIDConnect Authentication; setting samesite cookie to none and secure; that should work but i afraid that would raise CSRF (Cross site request Forgery) vulnerability for application.
Hence, An alternative is to switch to the Code response type which uses HTTP redirects and works with SameSite=Lax. Set the appropriate code response mode and response type.
ResponseMode = OpenIdConnectResponseMode.Query;
ResponseType = OpenIdConnectResponseType.Code;
https://github.com/aspnet/AspNetKatana/blob/635c92f641ad1e014eead31cc7a365004949fda5/src/Microsoft.Owin.Security.OpenIdConnect/OpenIdConnectAuthenticationOptions.cs#L65-L66

Setting Path and SameSite Attribute to All cookies of my Application

I am working on an ASP.Net Webforms Application. My security guy wants all cookies to have Path and SameSite attribute. I can set Path and SameSite attribute to the cookies which i have created but how do set it on Session Cookie. FOr eg whenver a visitor visits my site i set some Session Variables to ASP.net generates a session cookie on Get Request, how Do i set path and SameSite attribute on First Get Request. Is there any Global.asax event which i can use. I have tried using Application_PreSendRequestHeaders event like below
protected void Application_PreSendRequestHeaders()
{
foreach (string str in Response.Cookies.AllKeys)
{
HttpCookie ck = Request.Cookies[str];
ck.Path = "/DBTDASHBOARD";
ck.Value+=";SameSite=Strict";
}
}
But this works on subsequent requests and not the first get call made to my landing page. Any help would be of great use.
Finally I was able to do this with the help of url reweite 2.0 module for IIS.

AuthenticationProperties.RedirectUri is not passed to Google in Challenge()

Within my web application I have registered Google as a single sign-on provider:
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions {
ClientId = "8765.......apps.googleusercontent.com",
ClientSecret = "Secret"
})
My app doesn't allow users to sign-up/register (instead their accounts are created by an administrator, but they can later link their account up with Google).
In my "Sign in with Google" controller, I am trying to issue a Challenge() to redirect to Google. This might not be thecorrect approach:
string redirectUri = "http://localhost:55262/SSO/Google/ProcessToken"; // actually created in code, but shown as string for clarity
AuthenticationProperties properties = new AuthenticationProperties();
properties.RedirectUri = Server.UrlEncode(redirectUri);
Context.GetOwinContext().Authentication.Challenge(properties, "Google");
This correctly sends the user to Google, but Google then presents Error: redirect_uri_mismatch, saying that:
The redirect URI in the request: http://localhost:55262/signin-google
did not match a registered redirect URI.
I've seen this error before when the return URI collection in the Google control panel does not contain the redirect_uri specified.
If I debug in VS2015, I can see the redirect_uri property being set correctly in the AuthenticationProperties, but it seems that OWIN/Katana is not passing it to Google. Instead, when I hit Google, the return_uri is the default one used by OWIN/Katana. The one I set is being ignored.
The Google request details seem to confirm this:
scope=openid profile email
response_type=code
redirect_uri=http://localhost:55262/signin-google
What am I doing wrong here please? Should I not be using Challenge() to allow users to link up their local application account with Google?
To provide additional information on the accepted answer...
Its okay to ignore /signin-google
It emerges that the /signin-google URI is internally-managed by OWIN/Katana. You, as a developer, do not need to be concerned by it, but you do need to add it in the Google developer console as an Authorized redirect URI.
In the Google request, note that OWIN always passes the redirect URI to Google as /signin-google, regardless of what custom URI you set in the AuthenticationProperties.RedirectUri property. Although at first this may seem like a bug/problem, it has a major advantage in that OWIN can manage all callbacks via a single callback URI. Your callback URI is not forgotten about either (see below)!.
So what about your own redirect URL?
Well, that's where the AuthenticationProperties() come into play. By specifying your own callback URL like so...
AuthenticationProperties properties = new AuthenticationProperties { RedirectUri = "https://my.app.com/custom/callback/uri" };
...after OWIN has examined the Google token and extracted the necessary details, the user is then redirected to your specified URL.
This was where I was getting confused, as I didn't understand what to do with /signin-google, when in actual fact no action was taken. This applies to both MVC and webforms - you do not need to concern yourself with what gets passed to Google. However, if using webforms, or specifying authorization rules in web.config, you will need this to prevent returning users hitting the logging page again:
<location path="signin-google">
<system.web>
<authorization>
<allow users="*"/>
</authorization>
</system.web>
</location>
Here is all the code you need to send the user to Google, and return the token containing their details:
Outbound
Send the user to Google from a controller, button click event, page load, anything (regardless of your ASP/hosting stack):
// set the callback, for after OWIN finishes examining what comes back from Google
AuthenticationProperties properties = new AuthenticationProperties { RedirectUri = "https://www.myapp.com/some/callback/uri" };
// send the user to Google
Context.GetOwinContext().Authentication.Challenge(properties, "Google");
// Stop execution of the current page/method - the 401 forces OWIN to kick-in and do its thing
Response.StatusCode = 401;
Response.End();
Inbound
The user is returned from Google after validating their identity
Microsoft.AspNet.Identity.Owin.ExternalLoginInfo loginInfo = Context.GetOwinContext().Authentication.GetExternalLoginInfo();
Note that the OWIN's Open Authentication have predefined methods. In another words, in localhost:port/signin-google, the OWIN awaits for calling the signin-google by the external authentication service (Although you can't find its implementation inside the project). The signin-google is a valid and working path and I prefoundly exhort you not to change it (due to avoid writing a new implementation as a controller action).
I had similar trouble, After spending many weary days, finally, I found out the problem comes from the original user's URL which is effective on the sent redirect_uri by the OWIN. Clearly:
If you type www.site.com → redirect_uri equals to
www.site.com/signin-google
If you type site.com → redirect_uri equals to
site.com/signin-google
And Google will return redirect_uri_mismatch Error for one of the above cases based on entered redirect URLs in Console. I think your problem comes from this reality too and the solution is setting any possible URLs in console.

HttpModule not receiving cookies on IIS 6

I have an HttpModule that I created and am running on IIS 6 using Windows Server 2003. I can send cookies to the browser, but I can not read them on the next request, cookie is always null.
If I run this module on IIS 7 though, it works just fine. IIS 7 not an option at the moment as we have not switched over and this needs to get done before that will happen.
Also, I've already tried using the PostAcquireRequestState hook.
public void Init(HttpApplication httpApp)
{
httpApp.BeginRequest += OnBeginRequest;
}
public void OnBeginRequest(Object sender, EventArgs e)
{
var httpApp = (HttpApplication)sender;
var context = httpApp.Context;
const string cookieName = "sId";
if (!string.IsNullOrEmpty(context.Request.QueryString["cookie"]))
{
var ck = new HttpCookie(cookieName)
{
Value = httpApp.Context.Request.QueryString["cookie"],
Expires = DateTime.Now.AddDays(1)
};
httpApp.Response.Cookies.Add(ck);
}
else
{
var cookie = httpApp.Request.Cookies[cookieName]
}
}
I ran into a similar problem, but had a different solution, so I thought I'd share, in case it helps someone. I took zengchun's suggestion as well to use some tools to inspect request & response headers. Since I'm using IE, the F12 Dev Tools works great for this. As soon as I saw the response header for the cookie, I noticed the secure flag was set. Sure enough, I had copied code from a production SSL-hosted site to a test site that did not use SSL, so the secure flag on the cookie prevented the code from being able to read it. I updated the web.config to remove requireSSL from the httpcookies node, and my site started working. :)
your code look worked.the problem may be occur in the client-side how to request the next page.you can use the firebug with firefox or the fidder tools that can log your client-side request and see the request whether send cookd value in the request header to the server.
for example
the request headers:
get /1.aspx
.....
Cookie: sId=123 [if the client has a cookie then it will appear in here.]
the response headers:
Set-Cookie: sId=123; expires=Fri, 30-Mar-2012 07:20:23 GMT;
path=/
if the server add cookie to the response,then response it look like the above.
now,i guess the problem in your cook domain or you cookie path is different.
the best method to set cookie is like the follow code:
var ck = new HttpCookie(cookieName)
{
Value = httpApp.Context.Request.QueryString["cookie"],
Expires = DateTime.Now.AddDays(1),
Path="/",
Domain="your domain"
};
good luck.
Thanks to zhengchun I was able to get to the root of the problem. It turns out I was unable to set the cookie using requests to static files. I created .aspx files for my initial requests that redirected to the static files after setting the cookie. My HttpModule could then read the cookie after being set in the .aspx file. Not sure why I need a .aspx file to set the cookie instead of the HttpModule, but this fixed it.

Is it possible to delete subdomain cookies?

If there is a cookie set for a subdomain, metric.foo.com, is there a way for me to delete the metric.foo.com cookie on a request to www.foo.com? The browser (at least Firefox) seems to ignore a Set-Cookie with a domain of metric.foo.com.
Cookies are only readable by the domain that created them, so if the cookie was created at metric.foo.com, it will have to be deleted under the same domain as it was created. This includes sub-domains.
If you are required to delete a cookie from metric.foo.com, but are currently running a page at www.foo.com, you will not be able to.
In order to do this, you need to load the page from metric.foo.com, or create the cookie under foo.com so it can be accessable under any subdomain. OR use this:
Response.cookies("mycookie").domain = ".foo.com"
...while creating it, AND before you delete it.
..untested - should work.
I had the same problem with subdomains. For some reason getting the cookie first from the request didn't work. Instead I ended up just creating a new cookie with the same cookie name, and expiry date in the past. That worked perfectly:
void DeleteSubdomainCookie(HttpResponse response, string name)
{
HttpCookie cookie = new HttpCookie(name);
cookie.Expires = DateTime.Now.AddMonths(-1);
cookie.Domain = ".yourdomain.com";
response.Cookies.Add(cookie);
}

Resources