Iv'e been setting and retrieving cookies over pre-WebSocket stage to identify a user. I assumed everything would work as over a typical HTTP exchange.
This has worked flawlessly on all browsers I've tested them on, but reports started coming in that on iPhones the sign-ins would not be retained at all, signifying that cookies either were not set or sent back to the server.
// fret not, safety checks removed for brevity
const (
sessionKeyCookieName string = "session-key"
webSocketPath string = "/ws"
)
func serveWs(w http.ResponseWriter, r *http.Request) {
var sessionKey [sha1.Size]byte
var u *user
for _, cookie := range r.Cookies() {
if cookie.Name != sessionKeyCookieName {
continue
}
slice, err := base64.StdEncoding.DecodeString(cookie.Value)
if err != nil {
continue
} else {
copy(sessionKey[:], slice)
}
}
u, _ = getUserBySessionKey(sessionKey)
// regenerate key. TODO: does that add security?
rand.Read(sessionKey[:])
header := make(http.Header)
header.Add("Set-Cookie", (&http.Cookie{
Name: sessionKeyCookieName,
Value: base64.StdEncoding.EncodeToString(sessionKey[:]),
MaxAge: int(sessionLength.Seconds()),
HttpOnly: true,
Domain: strings.SplitN(r.Host, ":", 2)[0],
}).String())
ws, err := upgrader.Upgrade(w, r, header)
if err != nil {
if _, ok := err.(websocket.HandshakeError); !ok {
log.Println(err)
}
return
}
// do things to `user` so their messages go to where they're needed
go c.writePump()
c.readPump()
}
Headers as seen on Firefox network dev tool
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: eSazcZyZKj2dfa2UWSY+a4wThC8=
Access-Control-Allow-Origin: *
Set-Cookie: session-key=RNStK2z2gAsan7DyNKQ+efjyr7c=; Domain=redacted.org; Max-Age=259200; HttpOnly
Am I skipping some step that would allow Safari to store cookies, or is this an issue upstream1?
P.S. I'd really like to retain this approach, since I can use HTTP-only cookies and that mostly ensures that JavaScript has no access to them.
Looks like Gary is having similar issues as well. In short, cookies don't travel back over WebSockets.
TLDR: it's the HttpOnly flag.
It appears that while some browsers do allow Set-Cookie header in a response for WebSocket connection to have HttpOnly flag, iOS Safari considers the situation as "non-HTTP" and blocks this.
Interestingly, while setting HttpOnly cookies is not possible, HttpOnly cookies are sent in request headers while connecting a WebSocket. This leaves a pair of options:
Increase risk and omit HttpOnly;
Set your cookies with another plain HTTP request, quite possibly one that doesn't even have a response body.
I'd consider iOS Safari's behavior to be incorrect compared to what's outlined in RFC 6265 Storage model
If your Set-Cookie header works in other browsers my guess is it's an upstream issue, specifically iOS Safari has the ability to block cookies. By default iOS Safari blocks 3rd party cookies.
Can a webpage in mobile Safari check whether Settings > Safari > Accept Cookies 'From visited' or 'Always' is selected?
If cookies are blocked you can't use them. If you need cookies, detect support by setting a cookie on the login page like enabled=1 and then check for it in /ws handler. If it comes up blank and cookies are blocked you can try redirecting to /please-enable-cookies to ask the user to enable cookies for your site.
Another option is to store signed session data in local storage and include it in each request in the Authorization header. https://jwt.io/
Related
I have spent the entire day looking for an answer to this, and i have not been able.
I am trying to learn Flutter as a front-end framework. in the past i built my webpages with html, css, and vanilla js, but i was able to interpolate dynamic data from the server with php.
I would like to use httpOnly cookies for authenticating user sessions, as i have in the past with php. I'm open to other secure methods, but i am not interested in JWT.
Obviously i cannot access httpOnly cookies from my Flutter App, but i can access them with Golang, which i use for serving the html build, and on my api endpoints.
What i want to do is either, read the httpOnly cookie in golang, and then pass some information into my flutter build to be parsed at runtime, or somehow return an httpOnly cookie from an api endpoint, and persist that to the client that made the call, but i cannot for the life of me figure out how to do either.
I am open to ANY suggestions.
tl;dr on a server with golang backend and flutter front end, how do i read an httpOnly cookie in golang and based on that value, populate values in the flutter front end at runtime, or pass a variable from golang http.HandleFunc that can be read by Flutter at runtime?
you can access a cookie by its name in Go through the request r *http.Request
var myToken string
cookie, err := r.Cookie("myToken")
if err != nil {
myToken = ""
} else {
myToken = cookie.Value
}
you can then do user lookup or anything with that token inside a middleware and pass it to the next handler
ctxWithUser = context.WithValue(r.Context(), "user", authorizedUser)
rWithUser = r.WithContext(ctxWithUser)
next.ServeHTTP(w, rWithUser)
finally in your handler get the request context value
ctxUser := r.Context().Value("user").(user.User)
Background:
Let's have a WebAssembly (wasm) originating from .net code.
This wasm uses HttpClient and HttpClientHandler to access a backend API at https://api.uri.
The actual backend API location might change in time (like https://api.uri/version-5), but there is still this fixed endpoint, which provides redirection (3xx response) to the current location (which is in the same domain).
The API allows CORS, meaning it sends e.g. Access-Control-Allow-Origin: * headers in the responses.
In the normal (non-wasm) world, one just:
Plainly GETs the https://api.uri with no additional headers (CORS safe).
Retrieve the Location: header (containing e.g. https://api.uri/version-5) from the 3xx response as the final URI.
GETs/POSTs the final URI with additional headers (as needed, e.g. custom, auth, etc.).
Note: In ideal world, the redirection is handled transparently and the first two steps can just be omitted.
Although in the wasm world:
You are not allowed to (let the wasm/browser) send the OPTIONS pre-flight requests to a redirecting endpoint (https://api.uri).
You can't send any non-cors headers, when wanting to prevent pre-flight requests (reason for two stages, plain and full, described above).
You can't see the Location: header value (like https://api.uri/version-5) when trying the manual redirection (HttpClientHandler.AllowAutoRedirect = false), because the response is just artificially crafted with HTTP status code of 0 and ReasonPhrase == "opaqueredirect" - adoption to browser's Fetch API. What a nonsense! #1...
You can't see the auto-followed Location: header value in response.RequestMessage?.RequestUri, when trying the (default) automatic redirection (HttpClientHandler.AllowAutoRedirect = true), because there is still the original URI (https://api.uri) instead of the very expected auto-followed one (https://api.uri/version-5). What a nonsense! #2...
You can't send the full blown request with all the headers and rely on the automatic redirection, because it would trigger pre-flight, which is sill not allowed on redirecting endpoint.
So, the obvious question is:
Is there ANY way, how to handle such simple scenario from the Web Assembly?
(and not crash on CORS)
GET https://api.uri => 3xx, Location: https://api.uri/version-5
GET https://api.uri/version-5, Authorization: Basic BlaBlaBase64= ; Custom: Cool-Value => 200
Note: All this has been discovered within the Uno Platform wasm head, but I believe it applies for any .net wasm.
Note: I also guess "disabled" CORS (on the request side, via Sec-Fetch-Mode: no-cors) wouldn't help either, as then such request is not allowed to have additional headers/methods, right?
When my application retrieves index.html and other js and css files from the server, I do not see an csrf token in headers or cookies. How does play sends csrf token?
My UI is an Angular application being served from play. From the documents, I read about csrf token that
This token gets placed either in the query string or body of every form submitted, and also gets placed in the user’s session
The documentation also says that
To ensure that a CSRF token is available to be rendered in forms, and sent back to the client, the global filter will generate a new token for allGETrequests that accept HTML, if a token isn’t already available in the incoming request. - But I don't see this token in response to my initial GET request.
As my UI (and thus form) is not a play UI, I cannot use play's annotation to put csrf token in the form. I would like that when the homepage is delivered, play sends the csrf token which Angular application can store and use later.
Following are the headers I see on browser's developer console.
Response headers
Content-Length 1421
Content-Type text/html; charset=UTF-8
Date Sun, 11 Mar 2018 21:23:52 GMT
Referrer-Policy origin-when-cross-origin, strict-origin-when-cross-origin
X-Content-Type-Options nosniff
X-Frame-Options DENY
X-Permitted-Cross-Domain-Policies master-only
X-XSS-Protection 1; mode=block
Request headers (600 B)
Accept text/html,application/xhtml+xm…plication/xml;q=0.9,*/*;q=0.8
Accept-Encoding gzip, deflate
Accept-Language en-US,en;q=0.5
Connection keep-alive
Cookie PLAY_SESSION=eyJhbGciOiJIUzI1N…AR2uh5KwKBhqKxQQT1wWPWC2yPyCM
Host localhost:9000
Upgrade-Insecure-Requests 1
User-Agent Mozilla/5.0 (Windows NT 10.0; …) Gecko/20100101 Firefox/58.0
The Action in play which servers the homepage is
def index =
Action { implicit request =>
val Token(name, value) = CSRF.getToken.get
println(s"Token name ${name}, value ${value}")
Ok(views.html.index("Your new application is ready."))
}
I can see (print) the token name and value but I am not sure if it is being sent in the Ok response.
This is a partial answer. The 3 csrf configurations of interest in play are token, cookie and header names
if none of the token, cookie and header of csrf properties are configured then the default values are csrfToken for token name), nothing gets configured for cookie and Csrf-Token for header
When token name is configured then play seem to send a PLAY_SESSION cookie. Eg token.name = "CJCsrfToken". In this case, the name of the token is CJCsrfToken instead of csrfToken. However, I couldn't find how csrfToken gets sent and how to retrieve it in the client. I have an Angular5 client and I couldn't get it to pass csrf when only token.name was configured in play.
If cookie name is configured, Play will store the csrf token in a cookie with the given name, instead of in the session. I suppose we should configure either token.name or cookie.name. Eg cookie.name = "CJCsrfCookie" means you should see a cookie with name CJCsrfCookie
Now if only cookie.name is configured but no header name is configured then Play expects that requests from client will contain the csrf token in header Csrf-Token (the default header name)
The code in Angular to accept the cookie and return header was
HttpClientXsrfModule.withOptions({ cookieName: 'CJCsrfCookie', headerName: 'Csrf-Token' }),
If you do not want to use default header name, configure the new name in header.name. This would be the name of the header to accept CSRF tokens from.
eg header.name = "CJCsrfHeader"
The code in Angular to accept the cookie and return header was
HttpClientXsrfModule.withOptions({ cookieName: 'CJCsrfCookie', headerName: 'CJCsrfHeader' }),
Note that for the Angular part, the url has to be relative. See this angular4 httpclient csrf does not send x-xsrf-token has
I have a grails 2.2.4 application. I wanted to enable CORS
So I installed cors plugin by having the following line in build config.
plugins {
runtime ':cors:1.1.8'
}
Then in the config.groovy
cors.headers = ['Access-Control-Allow-Origin': '*']
But after this when I run the application, CORS in not enabled. So I debugged the CORS plugin. The issue seems to be in CorsFilter class in the following method
private boolean checkOrigin(HttpServletRequest req, HttpServletResponse resp) {
String origin = req.getHeader("Origin");
if (origin == null) {
//no origin; per W3C spec, terminate further processing for both preflight and actual requests
return false;
}
The origin parameter in the above line is always null as the request does not have the parameter 'Origin'. Is there something i'm doing wrong? I'm not looking for the answer which says add a manual header with the name "Origin" since that is not exactly a proper fix
I'm quite new to CORS so appriciate the help.
In addition to Access-Control-Allow-Origin, and in addition to setting the Origin header on request, you probably need to specify these response headers as well:
Access-Control-Allow-Headers: accept
Access-Control-Allow-Headers: origin
Access-Control-Allow-Headers: content-type
Access-Control-Allow-Method: GET
Access-Control-Allow-Method: POST
Also make sure you respond to HTTP OPTIONS requests with these headers and a blank 200 OK response.
For now, let's assume that RestClient is sending the Origin header properly. It may still be getting stripped by your application. You can prevent this using the Access-Control-Allow-Headers: Origin header.
Most of the problems I have had with my web services is that the right headers are being sent, but they are stripped from the message by my web server. So I tend to adopt a shotgun approach of "allow everything" and then one by one remove what I don't need. My allow-headers header usually is pretty long and I end up having to include stuff like Content-Type, X-Requested-With and other junk before my requests will finally go through.
I further recommend that you test using something besides RestClient, if only as a sanity check. I use Postman, a free Chrome app, for all my messaging tests. It looks to me like the problem is with RestClient not sending the proper Origin header.
I'm trying to set a cookie in my application.
Here's the code that sets the cookie:
public HttpResponseMessage LogIn(UserLoginVM user)
{
// Do login stuff
var cookie = new CookieHeaderValue("STUPID-COOKIE", "12345");
cookie.Domain = Request.RequestUri.Host;
cookie.Path = "/";
cookie.HttpOnly = true;
// Get user's profile
HttpResponseMessage res = Request.CreateResponse<UserProfileVM>(HttpStatusCode.OK, profile);
res.Headers.AddCookies(new CookieHeaderValue[] { cookie });
return res;
}
The response from the server is the following:
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
Set-Cookie: STUPID-COOKIE=12345; domain=localhost; path=/; httponly
Access-Control-Allow-Origin: *
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?QzpcUFJPSkVDVFNcU2Ftc2tpcC5TZXJ2aWNlV2ViTmV3XFNhbXNraXAuQXV0aEFQSVxTYW1za2lwLkF1dGhBUElcbG9naW4=?=
X-Powered-By: ASP.NET
Date: Wed, 18 Feb 2015 11:58:07 GMT
Content-Length: 8019
Notice the following header:
Set-Cookie: STUPID-COOKIE=12345; domain=localhost; path=/; httponly
However, when I go under "Cookies" in "Resources" tab in Chrome, nothing is set. Also when I send a request to the server no cookie is in the headers.
Here's the code that reads the cookie:
CookieHeaderValue cookie = Request.Headers.GetCookies("STUPID-COOKIE").FirstOrDefault();
cookie variable is always null.
My application is running on http://localhost:53998 and the authentication service is running on http://localhost:60858
My Chrome version is 40.0.2214.111.
Here's a GIF demonstrating the problem:
http://i.imgur.com/q7lkXBz.gif
Edit: This seems to be non-specific to Chrome. This doesn't work on FireFox (v35) either. GIF: http://i.imgur.com/ZewnEtc.gif
I ran into this issue today and Gaui's answer was quite useful to me, bearing in mind ideally you do not want to open up your server to CORS requests from any site. I'm in my dev environment and my server and client are on different origins since they are on different ports on localhost. Also I'm using .net core 3.0
My issue was caused by my server not sending cookies to my client side as my CORS settings were blocking the sending of cookie to my domain this was evident by the server not using the header Access-Control-Allow-Credentials: true. To resolve this I configured my server in Startup.cs to allow requests from my client side to allow credentials (A credential is a cookie's authorization headers or TLS client certificates).
var allowedOrigins = new[] {"localhost:3000"}; // Ideally comes from appsettings
app.UseCors(builder =>
builder.WithOrigins(allowedOrigins).AllowCredentials().AllowAnyMethod().AllowAnyHeader().Build());
For the cookie options; I found that the you do not have to set Domain if you do not want to, Secure works even when the site is not using https.
Google chrome now supports cookies on localhost, I believe it didn't used to as a lot of older SO posts have users who faced that issue.
On the client side, you need to configure it to accept cookies as well, as in Gaui's answer above. I was using fetch, and so had to add the option:
credentials: 'include'
Which tells fetch to retrieve cookies across domains. See the docs here
I highly suspect that localhost is not a valid domain name so Chrome rejects it. If you simply remove 'domain=localhost' from the Set-Cookie then it will work and Chrome will assign the domain to localhost for you.
I would personally create a local domain name like "test.dev" and add it to your Windows hosts file, 127.0.0.1 test.dev
I finally managed to solve this.
In the response from the API I had to add Access-Control-Allow-Credentials: true headers, or by adding the following in the WebApiConfig class:
public static void Register(HttpConfiguration config)
{
var cors = new EnableCorsAttribute("*", "*", "*");
cors.SupportsCredentials = true;
config.EnableCors(cors);
}
Then I had to enable it on the client-side, by setting the withCredentials property in the XMLHTTPRequest object to true. In AngularJS app config function you can do the following:
$httpProvider.defaults.withCredentials = true;
Also, for Chrome, I had to omit the Domain (or set it as null).
Hope this helps others struggling with this.