Browser not saving cookie sent by Golang backend - http

I know this question has been asked a bunch of times, but I tried most of the answers and still can't get it to work.
I have a Golang API with net/http package and a JS frontend. I have a function
func SetCookie(w *http.ResponseWriter, email string) string {
val := uuid.NewString()
http.SetCookie(*w, &http.Cookie{
Name: "goCookie",
Value: val,
Path: "/",
})
return val
}
This function is called when the user logs in, and I expect it to be sent to all the other endpoints. This works as expected with Postman. However, when it comes to the browser, I can't seem to get it to remember the cookie or even send it to other endpoints.
An example of JS using an endpoint
async function getDataWithQuery(query, schema){
let raw = `{"query":"${query}", "schema":"${schema}"}`;
let requestOptions = {
method: 'POST',
body: raw,
redirect: 'follow',
};
try{
let dataJson = await fetch("http://localhost:8080/query/", requestOptions)
data = await dataJson.json();
}catch(error){
console.log(error);
}
return data;
}
I tried answers like setting SameSite attribute in Golang, or using credential: "include" in JS with no luck.

Thanks to the discussion in the comments, I found some hints about the problem.
Saving cookies (both API and frontend on the same host)
I used document.cookie to save the cookie. I set the options by hand since calling res.cookie on the response of the API fetch only returned the value. An example is document.cookie = `goCookie=${res.cookie}; path=/; domain=localhost;.
Sending cookies
This has been answered before in previous questions and answered again in the comments. The problem was that I used credential:'include' instead of the correct credentials:'include' (plural).
CORS and cookies
In case the API and the frontend are not on the same host you will have to modify both the API and the frontend.
frontend
The cookie has to have the domain of the API since it's the API that requires it, not the frontend. So, for security reasons, you can't set a cookie for a domain (API) from another domain (frontend). A solution would be redirect the user to an API endpoint that returns Set-Cookie header in the response header. This solution signals the browser to register that cookie with the domain attached to it (the API's domain, since the API sent it).
Also, you still need to include credentials:'include' in the frontend.
API
You will need to set a few headers. The ones I set are
w.Header().Set("Access-Control-Allow-Origin", frontendOrigin)
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, withCredentials")
w.Header().Set("Access-Control-Allow-Methods", method) // use the endpoint's method: POST, GET, OPTIONS
You need to expose the endpoint where the frontend will redirect the user and set the cookie in the response. Instead of setting the domain of the API by hand, you can omit it, the browser will fill it with the domain automatically.
To handle the CORS and let JS send the cookie successfully, you will have to set the SameSite=None and Secure attributes in the cookie and serve the API over https (I used ngrok to make it simple).
Like so
func SetCookie(w *http.ResponseWriter, email string) string {
val := uuid.NewString()
http.SetCookie(*w, &http.Cookie{
Name: "goCookie",
Value: val,
SameSite: http.SameSiteNoneMode,
Secure: true,
Path: "/",
})
// rest of the code
}
I recommend you also read the difference between using localStorage and document.cookie, it was one of the problems I had.
Hope this helps.

Related

Sign in with Google - What should I do with `nonce`?

What I'm doing now:
Using the JavaScript API to render the button on my web page.
When the Sign in with Google flow is complete, my client-side JavaScript callback is called.
That callback sends the given .credentials string to my server.
The backend server (Node.js) calls the google-auth-library library's OAuth2Client.verifyIdtoken method on the .credentials string, which returns the user's email address (among other things), which my server uses to verify the user and create a session.
Everything works, but I'm wondering if there are any security concerns I'm missing. In particular there's a nonce field. The docs (link) don't explain how to use it.
Note: I'm using "Sign in with Google" and not the deprecated "Google Sign-In".
Edit: I'm familiar with the concept of nonces and have used them when doing the OAuth 2 server-side flow myself. What I can't figure out is how the Sign in with Google SDK expects me to use its nonce parameter with the flow above, where I'm using both their client-side and server-side SDKs.
Nonces are used as a CSRF-prevention method. When you make a request to Google, you include a nonce, and when authentication is complete, Google will send the same nonce back. The magic in this method is that if the nonce does not match what you sent then you can ignore the response, because it was probably spoofed.
Read more about CSRF here: https://owasp.org/www-community/attacks/csrf
Nonces are usually crytographically secure random strings/bytes.
I use crypto-random-string as a base to generate nonces, but any package with this functionality should suffice.
Sometimes I store nonces with a TTL in Redis, but other times I store nonces with an ID attached to the request so I can later verify it.
I'm telling you this since it took a bit long for me to figure out this nonce stuff :P
Using the example from Google's website (https://developers.google.com/identity/one-tap/android/idtoken-auth), I added the code for the nonce:
const nonce = '...'; // Supplied by client in addition to token
const {OAuth2Client} = require('google-auth-library');
const client = new OAuth2Client(CLIENT_ID);
async function verify() {
const ticket = await client.verifyIdToken({
idToken: token,
audience: CLIENT_ID, // Specify the CLIENT_ID of the app that accesses the backend
// Or, if multiple clients access the backend:
//[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
});
const payload = ticket.getPayload();
const serverNonce = payload['nonce'];
if (nonce != serverNonce) {
// Return an error
}
const userid = payload['sub'];
// If request specified a G Suite domain:
// const domain = payload['hd'];
}
verify().catch(console.error);

How to retrieve original host header on functions.https.Request?

I am working on a firebase cloud function that checks whether a user is signed in or not before allowing access to the requested page. If the Authentication header is not present in the HTTP request, then the user is redirected to the login page. Here is the code implemented:
exports.dashboard = functions.https.onRequest((req, res) => {
if ((!req.headers.authorization || !req.headers.authorization.startsWith('Bearer ')) {
res.redirect(req.headers.host + "/login.html")
}
else {
//check for Firebase ID Token and return the requested page
}
});
The problem with this code is that req.headers.host does not return the original HTTP Request host header value, instead it comes back with the cloud function server address in which the function is running.
I also implemented URL rewriting, so this cloud function is actually triggered by an HTTPS Request as follows:
"hosting": {
"rewrites": [ {
"source": "/dashboard.html",
"function": "dashboard"
} ]
}
EDIT
The fact that I chose to rewrite an HTTPS Request to a Cloud Function HTTPS Request could cause this issue? Not sure how Firebase Hosting handles URL rewriting internally, but it seems like a new request is triggered from cloud server so the original HTTPS Request from the browser is lost (at least the host header shows that). Using Firebase Emulator, the HTTPS Request is posted from localhost:5000, but when writing req.headers.host to the console (from onRequest(req,res) function) it outputs localhost:5001, which is Functions server emulator.
I found the answer in another post:
req.headers['x-forwarded-host']

How to make an authenticated PUT request to an App Engine service from an other origin

I have an App Engine service with a few methods implemented, where I restrict all routes with the login: admin option in the app.yaml.
Making a POST request to my service works:
fetch('http://localhost:8081/api/foo', {
credentials: 'include'});
But making a PUT request fails
await fetch('http://localhost:8081/api/foo', {
credentials: 'include',
method: 'PUT',
body: 'hi there'});
with the following error:
Response to preflight request doesn't pass access control check:
Redirect is not allowed for a preflight request.
I understand this is because my request is somehow not authenticated, and the server redirects my request to the login page. What I don't understand is how to authenticate it.
I'm using webapp2 to process the requests, and setting the following headers:
self.response.headers['Access-Control-Allow-Credentials'] = 'true'
self.response.headers['Content-Type'] = 'application/json'
# This feels wrong, but I still don't clearly understand what this header's purpose is...
self.response.headers['Access-Control-Allow-Origin'] = self.request.headers['Origin']
I think the deeper problem is that I don't undestand how this login feature works (is it cookie based? Why does it work with GET but not PUT? ...), and I don't truly understand CORS either.
Thanks for any help!
So, after discussing with Dan Cornilescu, here is the solution I came up with (Thanks Dan!)
Instead of having my classes inherit webapp2.RequestHandler, they inherit this custom HandlerWrapper.
The big difference is that when receiving an 'OPTIONS' request (ie. preflight), there is no login required. This is what was causing my problem: I couldn't get the preflight request to be authenticated, so now it doesn't need to be.
The CORS is also handled there, with a list of allowed origins
class HandlerWrapper(webapp2.RequestHandler):
def __init__(self, request, response):
super(HandlerWrapper, self).__init__(request, response)
self.allowed_origins = [
r'http://localhost(:\d{2,})?$', # localhost on any port
r'https://\w+-dot-myproject.appspot.com' # all services in the app engine project
]
self.allowed_methods = 'GET, PUT, POST, OPTIONS'
self.content_type = 'application/json'
# login mode: either 'admin', 'user', or 'public'
self.login = 'admin'
def dispatch(self):
# set the Allow-Origin header.
if self.request.headers.has_key('origin') and match_origin(self.request.headers['Origin'], self.allowed_origins):
self.response.headers['Access-Control-Allow-Origin'] = self.request.headers['Origin']
# set other headers
self.response.headers['Access-Control-Allow-Methods'] = self.allowed_methods
self.response.headers['Content-Type'] = 'application/json'
self.response.headers['Access-Control-Allow-Credentials'] = 'true'
# Handle preflight requests: Never require a login.
if self.request.method == "OPTIONS":
# For some reason, the following line raises a '405 (Method Not Allowed)'
# error, so we just skip the dispatch and it works.
# super(HandlerWrapper, self).dispatch()
return
# Handle regular requests
user = users.get_current_user()
if self.login == 'admin' and not users.is_current_user_admin():
self.abort(403)
elif self.login == 'user' and not user:
self.abort(403)
else:
super(HandlerWrapper, self).dispatch()
def match_origin(origin, allowed_origins):
for pattern in allowed_origins:
if re.match(pattern, origin): return True
return False
The login: admin configuration is based on the Users API, available only in the 1st generation standard environment. Not a CORS problem. From the login row in the handlers element table:
When a URL handler with a login setting other than optional
matches a URL, the handler first checks whether the user has signed in
to the application using its authentication option. If not, by
default, the user is redirected to the sign-in page. You can also use
auth_fail_action to configure the app to simply reject requests
for a handler from users who are not properly authenticated, instead
of redirecting the user to the sign-in page.
To use the Users API the user must literally login before the PUT request is made. Make a GET request first, which will redirect you to the login page, execute the login, then make the PUT request.
If that's not something you can achieve then you need to use a different authentication mechanism, not the one based on login: admin.
Update:
The above is true, but rather unrelated as the Users API authentication is addressed - you did mention that some other request method to the same URL works.
The error you get is indeed CORS-related, see Response to preflight request doesn't pass access control check. But I'd suggest not focusing on the accepted answer (which is only about working around CORS), but rather on this one, which is about doing CORS correctly.

Handling authentification to Firebase Database with Fetch in a Service Worker

I'm trying to query a Firebase database from a Service Worker using the Fetch API. However it doesn't work as expected as I can't get authenticated correctly.
Basically what I'm trying to do is from origin https://myproject.firebaseapp.com inside a Service Worker I do a call like this :
var fetchOptions = {};
fetchOptions.credentials = 'include';
var url = options.messageUrl;
var request = new Request('https://myproject.firebaseio.com/user/foobar.json', fetchOptions);
messagePromise = fetch(request).then(function(response) {
return response.json();
});
I'm getting this error :
Fetch API cannot load https://myproject.firebaseio.com/user/foobar.json. Response to preflight request doesn't pass access control check: Credentials flag is 'true', but the 'Access-Control-Allow-Credentials' header is ''. It must be 'true' to allow credentials. Origin 'https://myproject.firebaseapp.com' is therefore not allowed access.
Any idea of a way to fix it? How one should do to query/update the Firebase database from a SW?
I've read https://jakearchibald.com/2014/using-serviceworker-today/ and one of the gotcha was exactly that problem, the fact that Fetch request do not send authentification.
Ideally it would be great to be able to use the Firebase JS API inside a SW but this doesn't seem to work as well.
Firebase doesn't store authentication info as a cookie or in anything that would be sent along in the credentials, so there's no need to send them in your fetch request. Instead, you'll need to pull the token from Firebase Auth:
firebase.auth().currentUser.getToken(true).then(function(token) {
// token is the value you'll need to remember for later
});
Once you've got the token, you should be able to add it as a query parameter to the REST request e.g. ?auth={THE_TOKEN}. This will allow you to make your authenticated request in the Service Worker.

Meteor.user() on iron-router server side

How can check, on server side route, if user is logged?
I would add check on 'before', but Metor.user() don't work here.
thanks in advance.
p.s. I have found How to get Meteor.user() to return on the server side?, but not work on iron-router
I'm afraid that this is not possible. I guess that the problem comes from the fact that you're trying to connect to the server with two different protocols - both literally and in logically - so there is no obvious way to relate this two actions.
There is, however, a pretty simple solution that may suit your needs. You'll need to develop a simple system of privileges tokens, or secret keys, or whatever you call them. First, create a server method
var Secrets = new Meteor.Collection("secrets"); // only on server!!!
Meteor.methods({
getSecretKey: function () {
if (!this.userId)
// check if the user has privileges
throw Meteor.Error(403);
return Secrets.insert({_id: Random.id(), user: this.userId});
},
});
Then, you can now use it on the client to get the secretKey which attach to your AJAX request (or something), either within the HTTP header or in the URL itself. Fear not!
They will all be encrypted if you're using HTTPS.
On the server side you can now retrieve the secretKey from the incoming request and check if it is present in the Secrets collection. You'll know then if the user is granted certain privileges or not.
Also you may want to remove your secret keys from the collection after some time for safety reasons.
If what you're looking to do is to authenticate the Meteor.user making the request, I'm currently doing this within the context of IronRouter.route(). The request must be made with a valid user ID and auth token in the header. I call this function from within Router.route(), which then gives me access to this.user:
###
Verify the request is being made by an actively logged in user
#context: IronRouter.Router.route()
###
authenticate = ->
# Get the auth info from header
userId = this.request.headers['x-user-id']
loginToken = this.request.headers['x-auth-token']
# Get the user from the database
if userId and loginToken
user = Meteor.users.findOne {'_id': userId, 'services.resume.loginTokens.token': loginToken}
# Return an error if the login token does not match any belonging to the user
if not user
respond.call this, {success: false, message: "You must be logged in to do this."}, 401
# Attach the user to the context so they can be accessed at this.user within route
this.user = user
###
Respond to an HTTP request
#context: IronRouter.Router.route()
###
respond = (body, statusCode=200, headers={'Content-Type':'text/json'}) ->
this.response.writeHead statusCode, headers
this.response.write(JSON.stringify(body))
this.response.end()
This code was heavily inspired by RestStop and RestStop2. It's part of a meteor package for writing REST APIs in Meteor 0.9.0+ (built on top of Iron Router). You can check out the complete source code here:
https://github.com/krose72205/meteor-restivus

Resources