How do I use the cookie container with RestSharp and ASP.NET sessions? - c#

I'd like to be able to call an authentication action on a controller and if it succeeds, store the authenticated user details in the session.
However, I'm not sure how to keep the requests inside the session as I'm using RestSharp as a detached client. I need to somehow get a key back from the server on successful authorisation and then for each future call, check the key with that stored in the session.
How do I ensure the RestClient in RestSharp sends all future requests with the cookie set correctly so inside service calls, the session variable can be retrieved correctly?
I've been looking at the cookie container with HttpFactory but there doesn't seem to be any documentation on this anywhere.

If someone is having a similar problem, please note that the above is not quite required for a simple "store my cookies after each request" problem.
Jaffas approach above works, but you can simply attach a CookieStore to your RestClient and have the cookies be stored automatically.
I know this is not a solution for everyone, since you might want to store dedicated cookies only. On the other hand it makes your life easier for testing a REST client!
(I used Jaffas variables for ease):
CookieContainer _cookieJar = new CookieContainer();
var client = new RestClient("http://<test-server>/letteron"); //test URL
client.CookieContainer = _cookieJar;

I worked this out in the end.
Basically create a cookie container, then add the session cookie from the response into the cookie container. All future requests will then contain this cookie.
var sessionCookie = response.Cookies.SingleOrDefault(x => x.Name == "ASP.NET_SessionId");
if (sessionCookie != null)
{
_cookieJar.Add(new Cookie(sessionCookie.Name, sessionCookie.Value, sessionCookie.Path, sessionCookie.Domain));
}

Related

How to add a cookie to a request using HttpClient from IHttpClientFactory

Background
I'm working on an ASP.NET Core Web API where we within our API call a 3rd party API. This 3rd party API requires every request to contain a cookie with an access token. Our API gets this token from a claim (from the ClaimsPrincipal of the user associated with the request).
Details
This answer shows how to set a cookie on a request, but the example requires that one manually constructs the HttpClient (to be able to inject a HttpClientHandler with a CookieContainer). And since it is not desirable to do manual instantiation of the HttpClient, I would rather get the HttpClient injected through DI, which this example illustrates (usin IHttpClientFactory).
In my service, I have access to a an injected HttpClient instance (_httpClient), and the most simple function looks like this:
public async Task<string> GetStatusAsync(ClaimsPrincipal user)
{
// TODO: Add cookie with access token (from user's claims) before making request
return await _httpClient.GetStringAsync(Endpoints.Status);
}
This GitHub issue asks about how to include authorization cookie when using IHttpClientFactory, and the answer says to use the header propagation middleware. And now to my issue:
From what I can see, you set up the header propagation middleware (with all headers and cookies and whatnot) during service configuration at application startup. In our API however, I do not have the value for the authentication cookie before actually making the request to the 3rd party API.
Question
How can I add a cookie to the request on a HttpClient instance that is injected to my service using IHttpClientFactory, right before making the actual request?
The solution was indeed to use header propagation. I just had to get all the pieces together correctly.
Header propagation is configured inside ConfigureServices (in Startup.cs). And since I want to use data from the current user's claims, the trick is, when adding the cookie header, to use on of the overloads that takes in a function that gives you access to the current context:
services.AddHeaderPropagation(options =>
{
options.Headers.Add("Cookie", context =>
{
var accessToken = context.HttpContext.User.Claims.FirstOrDefault(c => c.Type == "access-token")?.Value;
return accessToken != null ? new StringValues($"token={accessToken}") : new StringValues();
});
});
In addition to this, since I'm using a Typed Service, I also had to configure that that specific service should forward the cookie header (at the same place inside ConfigureServices in Startup.cs):
services.AddHttpClient<IApiService, ApiService>(httpClient =>
{
httpClient.BaseAddress = new Uri("https://httpbin.org/");
}).AddHeaderPropagation(options =>
{
options.Headers.Add("Cookie");
});
And finally, the thing that caused me some trouble for a little while: Since I'm using data from the current users claims, the registration of the header propagation middleware (app.UseHeaderPropagation(); inside Configure in Startup.cs) must happen after adding the authentication middleware (app.UseAuthentication();). If not, the claims haven't been set yet.
And as a final tip: I head great us in https://httpbin.org/ when working on this. The request inspection endpoints there are really useful to see what data you actually pass along in your request.
You can put a static function inside AddHeaderPropagation config function:
services.AddHeaderPropagation(options =>
{
options.Headers.Add(MyCookie());
});
And inside MyCookie function,
get the current context end extract from them all needed data.
If you need something from your current flow or current controller you can add it
inside httpSession and take it inside MyCookie function.

How to make sure the user's original request URL is used after WS Federated authentication in OWIN app

I have an OWIN app that authenticates users with cookie authentication, and if that fails then it tries WS-Federated authentication. In other words, if the correct cookie isn't there in the initial request, then go to the STS and fetch the security token.
The problem I'm having is making sure the user's original URL request is fulfilled if federated authentication is used. Thanks to this post, I realized that the redirect URL from the STS back to the OWIN app contains the "original" URL in the wctx query parameter. The issue is that this value is set, as far as I can tell, using WsFederationAuthenticationOptions.Wtrealm (perhaps Wreply?). This is a problem because these values are set in the initial configuration. I don't want a hard coded value -- I just want the URL the user originally used (i.e. IOwinContext.Request.Uri). The documentation for Wtrealm and Wreply does not help explain what the values should be.
I thought I could sneakily set Wtrealm to the user's request URL before redirecting to the STS, but apparently it's already set by the time RedirectToIdentityProvider notification is raised:
RedirectToIdentityProvider = context =>
{
context.Options.Wtrealm = context.Request.Uri.ToString();
}
Does anyone know what the correct approach is? Is there a way to make Wtrealm in the initial configuration the user's request URL? Or is Wtrealm not what I think it is, and I should be approaching this in a different way?
I believe I figured it out. Wtrealm, as I suspected, isn't what's causing the issue; that is used to determine where the STS should reenter to finish the federated authentication, but there is another redirect URL that determines where to go after authenticating.
I dug through the Microsoft.Owin.Security.WsFederation source code using ILSpy, and I figured out that there was a property I was not setting: AuthenticationProperties.RedirectUri. This RedirectUri property needs to be set before redirecting to the STS, as it needs to be used later in InvokeAsync after authenticating. I have an AuthenticateAllRequests method and I initialize this AuthenticationProperties object there:
private static void AuthenticateAllRequests(IAppBuilder app, params string[] authenticationTypes)
{
app.Use((context, continuation) =>
{
if (context.Authentication.User?.Identity?.IsAuthenticated ?? false)
{
return continuation();
}
else
{
AuthenticationProperties authProps = new AuthenticationProperties
{
RedirectUri = context.Request.Uri.ToString()
};
context.Authentication.Challenge(authProps, WsFederationAuthenticationDefaults.AuthenticationType);
return Task.CompletedTask;
}
});
}
To summarize more concretely: let's say my OWIN startup URL is http://myapp.com/owin. So then I set Wtrealm = "http://myapp.com/owin". Let's say a user goes to http://myapp.com/owin/coolstuff, and let's say they are signed into the authentication server with SSO, but they don't have the cookie for my app. In this case WS-Federated authentication needs to be used. http://myapp.com/owin/coolstuff is set as AuthenticationProperties.RedirectUri before redirecting to the STS so that it is put in the wctx query parameter, and can therefore be used after returning back to the OWIN app.
I was getting confused before because after returning from the STS, I would see IOwinRequest.Uri = "http://myapp.com/owin", and I would assume that was the final URL, and that the user's original request was lost. Now it makes a lot more sense that that is the URL the STS redirects to in order to finish authentication, but the final redirect URL, http://myapp.com/owin/coolstuff, is stored for redirecting after authentication finishes.
I am pretty sure this is how it works, but I may be wrong and if anyone has anything to add I'd love to hear it.

HttpWebRequest: Add Cookie to CookieContainer -> ArgumentException (Parametername: cookie.Domain)

I'm trying to login to a website via my application.
What I did:
First I figured out how the browser does the authorization process with Fiddler.
I examined how the POST request is built and I tried to reconstruct it.
The browser sends 4 cookies (Google Analytics) and I tried to set them:
CookieContainer gaCookies = new CookieContainer();
gaCookies.Add(new Cookie("__utma", "#########.###########.##########.##########.##########.#"));
gaCookies.Add(new Cookie("__utmb", "#########.#.##.##########"));
gaCookies.Add(new Cookie("__utmc", "#########"));
gaCookies.Add(new Cookie("__utmz", "#########.##########.#.#.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none)"));
(just replaced the original cookie data with #)
Then I went through the code with the debugger and as soon as the first gaCookies.Add is executed, the application stops with an
System.ArgumentException: The parameter '{0}' cannot be an empty string. Parameter name: cookie.Domain
I would like to know why this happens. The constructor of Cookie doesn't require a domain and I don't know where I can get this value?
Would be very great if someone of you could help me with this.
I'm not a webdeveloper or an expert in web stuff so I don't know much about it.
Is there maybe a great source where I can learn about this if there is no "short and quick answer"?
CookieContainers can hold multiple cookies for different websites, therefor a label (the Domain) has to be provided to bind each cookie to each website. The Domain can be set when instantiating the individual cookies like so:
Cookie chocolateChip = new Cookie("CookieName", "CookieValue") { Domain = "DomainName" };
An easy way to grab the domain to is to make a Uri (if you aren't using one already) that contains your target url and set the cookie's domain using the Uri.Host property.
CookieContainer gaCookies = new CookieContainer();
Uri target = new Uri("http://www.google.com/");
gaCookies.Add(new Cookie("__utmc", "#########") { Domain = target.Host });

Difference between HttpResponse: SetCookie, AppendCookie, Cookies.Add

there are some different ways to create multi value cookies in ASP.NET:
var cookie = new HttpCookie("MyCookie");
cookie["Information 1"] = "value 1";
cookie["Information 2"] = "value 2";
// first way
Response.Cookies.Add(cookie);
// second way
Response.AppendCookie(cookie);
// third way
Response.SetCookie(cookie);
When should I use which way? I've read that SetCookie method updates the cookie, if it already exits. Doesn't the other ways update the existing cookie as well?
And is the following code best practice for writing single value cookies?
Response.Cookies["MyCookie"].Value = "value";
If I remember correctly both
Response.Cookies.Add(..)
and
Response.AppendCookie(..)
will allow multiple cookies of the same name to be appended to the response.
On the other hand
Response.SetCookie(..)
and
Response.Cookies[key].Value = value;
will always overwrite previous cookies of the same name.
When should I use which way?
It's depends on what Cookie operation you want to do.
Note that Add and AppendCookie are doing the same functionality except the fact that with AppendCookie you're not referencing the Cookies property of the Response class and it's doing it for you.
Response.Cookies.Add - Adds the specified cookie to the cookie
collection.
Response.AppendCookie - Adds an HTTP cookie to the
intrinsic cookie collection
Response.SetCookie - Updates an existing cookie in the cookie
collection.
Exceptions will not be thrown when duplicates cookies are added or when attempting to update not-exist cookie.
The main exception of these methods is: HttpException (A cookie is appended after the HTTP headers have been sent.)
The Add method allows duplicate cookies in the cookie collection. Use the Set method to ensure the uniqueness of cookies in the cookie collection.
Thanks for MSDN!
To piggyback on tne's comment in Wiktor's reply, AppendCookie and SetCookie shouldn't be used - they're for internal use by the .NET framework. They shouldn't be public but they are, my guess would be as a hack for the IIS pipeline somewhere else.
So you should do your cookie setting this way (or write an extension method for setting multiple cookies):
string cookieName = "SomeCookie";
string cookieValue = "2017";
if (Response.Cookies[cookieName] == null)
{
Response.Cookies.Add(new HttpCookie(cookieName, cookieValue));
}
else
{
Response.Cookies[cookieName].Value = cookieValue;
}

Create cookie with cross domain

I am working on cookies. I am able to create cookies very easily. To create a cookie I am using this code:
HttpCookie aCookie = new HttpCookie("Cookie name");
aCookie.Value = "Value";
Response.Cookies.Add(aCookie);
This code is fine for me and it gives me localhost as Host. But the problem comes here when I try to add a domain name here like:
HttpCookie aCookie = new HttpCookie("Cookie name");
aCookie.Value = "Value";
aCookie.Domain = "192.168.0.11";
Response.Cookies.Add(aCookie);
Now the cookie is not generated. Any suggestions?
You can only set the domain to yourself (the current site) and sub-domains of yourself, for security reasons. You can't set cookies for arbitrary sites.
As Marc has said - you can't do this; unless the domain is a subdomain of the one returning the response.
The same limitation applies to javascript code on the client adding cookies as well - the same origin policy will apply.
A simple way to achieve this is generally to include on the page returned from abc.com somewhere a reference to a resource on the domain xyz.com - typically a javascript file or something like that.
You have to watch out there, though, because that's a third-party cookie and some users will have those disabled (because it's how ad-tracking works).
Assuming you have a known set of cookies you want to track across domains and that you own all the domains you are sharing cookies with, you can build this functionality yourself. Here is poor man's cross-domain cookie tracking:
You can add "?favoriteColor=red" to all links on abc.com that point to xyz.com.
XYZ Contact
Then do the same thing for all links on xyz.com that point to abc.com.
ABC Contact
Now on every page of abc.com and xyz.com need to check the http request path for ?favoriteColor=red and if it exists, set the favoriteColor cookie on that domain to red.
// Pseudocode
if(queryString["favoriteColor"] != null) {
setCookie("favoriteColor", queryString["favoriteColor"]);
}
Tip 1: Do some validation to ensure that the value you get is valid because users can enter anything.
Tip 2: You should be using https if you're going to do this.
Tip 3: Be sure to url escape your cookie name and value in the url.

Categories