NodeJS API creates two sessions because of "CORS preflight request" - node.js

I am currently developing an API in NodeJS and a WebClient in VueJS.
I want to create a simple login/logout mechanism. The Webclient should send the request to the API and the API should handle the sessions of the different users and serving the data from its mongoDB.
Recently I came across a strange problem. When I want to login via WebClient, the browser shows me that it sends two different headers to the API. One "OPTIONS" header and one "POST" header. The POST header is sent due to my POST-Request (WebClient), which is clear. Due to Mozillas explenation I also understand the OPTION header part since the browser wants to know if the API's CORS-configuration has been configured contrary for the WebClient or not (or something like this).
But the problem now is the following:
Due to the two different header-methods, my API creates two session-IDs with just one login-post action (via WebClient), whereas one of these two sessions gets detached from the WebClient, unnecessarily consuming valuable space. This only happens through the WebClient. Using PostMan does not show this behaviour, only one session will be created since only one header is sent.
What I want to know is:
Since there is a reason for why the OPTIONS-header is sent, I want to know how I can prevent my API to create the second session via the WebClient.
Since this problem happened after testing my WebClient, it is clear to me that the WebClient is not configured or written properly, but I cannot tell where or how to prevent this since WebDev at this level is new to me. Like: Do I have to configure my WebClient or the API?
If more code is needed just tell me what you need and I will edit this post and attach the neede code.
//////////////////// Code:
//// API:
// src/main.js:
const corsOptions = {
origin: "http://localhost:8080",
credentials:true,
methods: "GET,HEAD,POST",
preflightContinue: false,
optionsSuccessStatus: 204
};
// src/routes/LoginRoute.js:
router.post("/login", function(req,res,next){
console.log("// Routes/Login");
if(!req.user){
console.log("---- Info: User is not logged in");
passport.authenticate("local", (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
return res.status(401).json({success:false,errors:["User not found"]});
}
req.login(user, (err) => {
if(err){
return next(err);
}
console.log("---- Info: Routing success");
return res.status(200).json({success:true});
});
})(req, res, next);
}else{
console.log("---- Info: User is already logged in");
return res.status(403).json({success:false,errors:["Already logged in"]});
}
});
//// VueJS
// src/store/index.js
actions:{
authenticate({commit},formData){
console.log("---- Info: Vuex/Store/Action/Authenticate - Trying to log in");
var url = "http://localhost:3000/api/login";
console.log(formData);
return Vue.axios.post(url,formData)
.then((res)=>{
console.log("---- Info: Vuex/Store/Action/Authenticate - Success");
commit('login',res.data);
return res.data;
})
.catch((err)=>{
console.log("---- Error: Vuex/Store/Action/Authenticate");
console.log(err);
// commit('logout');
return err;
});
}
}
//////////////////// FireFox Network Analysis:

I would believe that the cors package you apparently use would resolve this problem.
In any case this is not a problem with your frontend, it's handled by your backend and it's typical that the browser creates problems that aren't present with Postman. Postman is built to not care about browser CORS issues. You can however set a session in Postman for testing: https://blog.postman.com/sessions-faq/
Back to the problem: One approach is a middleware function for all or specific routes to filter out requests that already contain a session.
app.use((req, res, next) => {
if (req.session) {
// bypass new session creation, re-route or other solution
}
next()
})
Another and more flexible approach is to target the OPTIONS header directly. I solved a similar problem in a serverless proxy function with a request handler that targets the OPTIONS header specifically. It filters such requests out and returns an "OK signal" and generous headers to tell the browser it can go ahead with the real request, the POST.
You could try something like this as a general middleware or add it as a response to certain endpoints (code not tested, just freestyling the Express syntax here):
const optionsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type'
}
exports.reqHandler = async(req, res) => {
if (req.method === 'OPTIONS') {
return {
res.set(optionsHeaders)
res.status(200)
}
}
req.session.user = {}
req.session.user.id = uuid.v4()
// etc. ...
return res.status(200).json({
success: true,
data: req.session.user
msg: 'Session ID set'
})
}

So I figured out what to do in order to prevent the browser to send two headers to the API server.
I had to configure the axios.post() overgive a header option to the funciton:
const axiosHeaders = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
Vue.axios.post(url,QueryString.stringify({username:username}),axiosHeaders)
.then(..)
[...]
Setting the "Configure-Type" as "application/x-www-form-urlencoded" will do the work, but the form-data has to be wrapped with the QueryString.stringify(..) function.
With this, the browser stops to send multiple headers with one post-request and in my case, it stops to create multiple sessions on my API.
I hope this will be usefull for anyone else too.

Related

Cant get all response data from fetch

So I'm trying to store some info in the session, for later use. But when I fetch it in react, it doesn't all come back. Despite the get route having all of the data.
My thought process is on the serverside, when a user logs in store their id in the session. I'll then have a /userinfo route that dumps all the data i need in json. I can then fetch that /userinfo route and get the data in the response. My code is as follows:
Heres the post code to sign in, I console.log to verify the session is modified. It is.
User.authenticate(req.body.logemail, req.body.logpassword, function (error, user) {
if (error || !user) {
var err = new Error('Wrong email or password.');
err.status = 401;
return next(err);
} else {
req.session.userId = user._id;
console.log(req.session);
return res.redirect('/');
}
});
Then I have this route:
app.get('/userinfo', (req, res)=>{
res.json(req.session);
});
If I simply go to localhost:3000/userinfo the json looks like:
{
"cookie": {
"originalMaxAge": null,
"expires": null,
"httpOnly": true,
"path": "/"
},
"userId": "blahblahblah"
}
so UserID is indeed there.
However, when I finally go to fetch it in react with:
fetch('http://localhost:3000/userinfo')
.then(res => res.json())
.then(data => console.log(data));
All I get back is the cookie. I dont get UserId. I've tried adding UserId to the cookie as well, just to see if it'd work, and likewise I just get the plain cookie.
Solved the issue myself, I didn't even think about it, but I'm making the request from the client, so when I make the request through fetch()
to
app.get('/userinfo', (req, res)=>{
res.json(req.session);
});
the server is returning the session stored on the clientside, which hasnt been updated with userId. So I need to return session data from the serverside, rather than from the client request.
My solution is to create and maintain a User object on the server-side and when I need to access user data, grab it from the user object.

Is there a way to shut of a Middleware in node router.use() method when convenient?

So I just came to a problem in my node app where in the back-end I created a login and register post method that takes in username and password. Then I created a middleware router.use that use token in browser to get users profile information. The problem is everything bellow that middleware is now requres a token header authentication:
this.options = new RequestOptions({
headers: new Headers({
'Content-Type': 'application/json',
'authorization': this.auth.authToken
})
});
I want to know how I can bypass this middleware and not use header authentication to get my blogs that are posted in the database.
Here is the middleware.
router.use((req, res, next) => {
const token = req.headers['authorization'];
if (!token) {
res.json({
success: false,
message: 'No token'
});
} else {
jwt.verify(token, Data.secret, (err, decoded) => {
if (err) {
res.json({
success: false,
message: 'Token invalid: ' + err
});
} else {
req.decoded = decoded;
next();
}
});
}
});
Every post and get methods bellow this router middleware requires I use this middleware. Is there anyway I can bypass this middleware and not use a requestOptions authentication to get my blogs from database?
Thanks
Well, the obvious answer is to register the routes that do not need the token above this middleware, and those that do need the token below this middleware. Routes respect registration order.
Alternatively, your logic clearly sends a response if there's no token. You could also simply not do that and allow the future routes to handle the case that there is no token. It really depends on your use case.

Authentication on Server side routes in Meteor

What is the best way (most secure and easiest) to authenticate a user for a server side route?
Software/Versions
I'm using the latest Iron Router 1.* and Meteor 1.* and to begin, I'm just using accounts-password.
Reference code
I have a simple server side route that renders a pdf to the screen:
both/routes.js
Router.route('/pdf-server', function() {
var filePath = process.env.PWD + "/server/.files/users/test.pdf";
console.log(filePath);
var fs = Npm.require('fs');
var data = fs.readFileSync(filePath);
this.response.write(data);
this.response.end();
}, {where: 'server'});
As an example, I'd like to do something close to what this SO answer suggested:
On the server:
var Secrets = new Meteor.Collection("secrets");
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});
},
});
And then in client code:
testController.events({
'click button[name=get-pdf]': function () {
Meteor.call("getSecretKey", function (error, response) {
if (error) throw error;
if (response)
Router.go('/pdf-server');
});
}
});
But even if I somehow got this method working, I'd still be vulnerable to users just putting in a URL like '/pdf-server' unless the route itself somehow checked the Secrets collection right?
In the Route, I could get the request, and somehow get the header information?
Router.route('/pdf-server', function() {
var req = this.request;
var res = this.response;
}, {where: 'server'});
And from the client pass a token over the HTTP header, and then in the route check if the token is good from the Collection?
In addition to using url tokens as the other answer you could also use cookies:
Add in some packages that allow you to set cookies and read them server side:
meteor add mrt:cookies thepumpinglemma:cookies
Then you could have something that syncs the cookies up with your login status
Client Side
Tracker.autorun(function() {
//Update the cookie whenever they log in or out
Cookie.set("meteor_user_id", Meteor.userId());
Cookie.set("meteor_token", localStorage.getItem("Meteor.loginToken"));
});
Server Side
On the server side you just need to check this cookie is valid (with iron router)
Router.route('/somepath/:fileid', function() {
//Check the values in the cookies
var cookies = new Cookies( this.request ),
userId = cookies.get("meteor_user_id") || "",
token = cookies.get("meteor_token") || "";
//Check a valid user with this token exists
var user = Meteor.users.findOne({
_id: userId,
'services.resume.loginTokens.hashedToken' : Accounts._hashLoginToken(token)
});
//If they're not logged in tell them
if(!user) return this.response.end("Not allowed");
//Theyre logged in!
this.response.end("You're logged in!");
}, {where:'server'});
I think I have a secure and easy solution for doing this from within 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, or responds with a 401 if the authentication fails:
// 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) ->
this.response.statusCode statusCode
this.response.setHeader 'Content-Type', 'text/json'
this.response.writeHead statusCode, headers
this.response.write JSON.stringify(body)
this.response.end()
And something like this from the client:
Meteor.startup ->
HTTP.get "http://yoursite.com/pdf-server",
headers:
'X-Auth-Token': Accounts._storedLoginToken()
'X-User-Id': Meteor.userId()
(error, result) -> // This callback triggered once http response received
console.log result
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
Because server-side routes act as simple REST endpoints, they don't have access to user authentication data (e.g. they can't call Meteor.user()). Therefore you need to devise an alternative authentication scheme. The most straightforward way to accomplish this is with some form of key exchange as discussed here and here.
Example implementation:
server/app.js
// whenever the user logs in, update her apiKey
Accounts.onLogin(function(info) {
// generate a new apiKey
var apiKey = Random.id();
// add the apiKey to the user's document
Meteor.users.update(info.user._id, {$set: {apiKey: apiKey}});
});
// auto-publish the current user's apiKey
Meteor.publish(null, function() {
return Meteor.users.find(this.userId, {fields: {apiKey: 1}});
});
lib/routes.js
// example route using the apiKey
Router.route('/secret/:apiKey', {name: 'secret', where: 'server'})
.get(function() {
// fetch the user with this key
// note you may want to add an index on apiKey so this is fast
var user = Meteor.users.findOne({apiKey: this.params.apiKey});
if (user) {
// we have authenticated the user - do something useful here
this.response.statusCode = 200;
return this.response.end('ok');
} else {
// the key is invalid or not provided so return an error
this.response.statusCode = 403;
return this.response.end('not allowed');
}
});
client/app.html
<template name="myTemplate">
{{#with currentUser}}
secret
{{/with}}
</template>
Notes
Make /secret only accessible via HTTPS.
While it's very likely that the user requesting /secret is currently connected, there is no guarantee that she is. The user could have logged in, copied her key, closed the tab, and initiated the request sometime later.
This is a simple means of user authentication. I would explore more sophisticated mechanisms (see the links above) if the server-route reveals high-value data (SSNs, credit cards, etc.).
See this question for more details on sending static content from the server.
I truly believe using HTTP headers are the best solution to this problem because they're simple and don't require messing about with cookies or developing a new authentication scheme.
I loved #kahmali's answer, so I wrote it to work with WebApp and a simple XMLHttpRequest. This has been tested on Meteor 1.6.
Client
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
// Skipping ahead to the upload logic
const xhr = new XMLHttpRequest();
const form = new FormData();
// Add files
files.forEach((file) => {
form.append(file.name,
// So BusBoy sees as file instead of field, use Blob
new Blob([file.data], { type: 'text/plain' })); // w/e your mime type is
});
// XHR progress, load, error, and readystatechange event listeners here
// Open Connection
xhr.open('POST', '/path/to/upload', true);
// Meteor authentication details (must happen *after* xhr.open)
xhr.setRequestHeader('X-Auth-Token', Accounts._storedLoginToken());
xhr.setRequestHeader('X-User-Id', Meteor.userId());
// Send
xhr.send(form);
Server
import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import { Roles } from 'meteor/alanning:roles'; // optional
const BusBoy = require('connect-busboy');
const crypto = require('crypto'); // built-in Node library
WebApp.connectHandlers
.use(BusBoy())
.use('/path/to/upload', (req, res) => {
const user = req.headers['x-user-id'];
// We have to get a base64 digest of the sha256 hashed login token
// I'm not sure when Meteor changed to hashed tokens, but this is
// one of the major differences from #kahmali's answer
const hash = crypto.createHash('sha256');
hash.update(req.headers['x-auth-token']);
// Authentication (is user logged-in)
if (!Meteor.users.findOne({
_id: user,
'services.resume.loginTokens.hashedToken': hash.digest('base64'),
})) {
// User not logged in; 401 Unauthorized
res.writeHead(401);
res.end();
return;
}
// Authorization
if (!Roles.userIsInRole(user, 'whatever')) {
// User is not authorized; 403 Forbidden
res.writeHead(403);
res.end();
return;
}
if (req.busboy) {
// Handle file upload
res.writeHead(201); // eventually
res.end();
} else {
// Something went wrong
res.writeHead(500); // server error
res.end();
}
});
I hope this helps someone!
Since Meteor doesn't use session cookies, client must explicitly include some sort of user identification when making a HTTP request to a server route.
The easiest way to do it is to pass userId in the query string of the URL. Obviously, you also need to add a security token that will prove that the user is really who the claim they are. Obtaining this token can be done via a Meteor method.
Meteor by itself doesn't provide such mechanism, so you need some custom implementation. I wrote a Meteor package called mhagmajer:server-route which was thoroughly tested. You can learn more about it here: https://blog.hagmajer.com/server-side-routing-with-authentication-in-meteor-6625ed832a94

express passport-linkedin, making api requests

I'm using express and passport-linkedin. The authentication flow works fine, I can get the user's basic profile, and now I want to make an API request. /v1/people/~/connections, for example.
I've done some reading and trial and error, but all I get is Wrong Authentication Scheme or Internal Server Error.
I have the token and tokenSecret given to the verify callback of the LinkedInStrategy instance. How do I go from those to an authorized API request?
I use a 'isLegit' function running as middleware.
Example :
app.get('/api/mysecuredpage',isLegit,function(req,res){
res.render('mysecurepage');
});
function isLegit(req, res, next) {
// if user is authenticated in the session, next
if (req.isAuthenticated())
return next();
// if they aren't send an error
res.json({error:'You must be logged in to view information'});
}
EDIT :
To make a request to the linkedIn api, just set up your request with the following options :
var options = {
url: 'https://api.linkedin.com/v1/people/~/connections',
headers: { 'x-li-format': 'json' },
qs: { oauth2_access_token: user.access_token }
};
request(options,function(err,res,body){
console.log(body);
});
access_token is the name of the variable in your passport strategy, it could be different depending how you've set it up. basically, it's one of the fields of your authenticated user ;-)

express.js not saving session data on nodeunit tests

Update at bottom!
My node.js server uses express.js to manage sessions. Upon login, I store some user information in req.session. I have a logout endpoint that simply deletes the user data from req.session before sending its response.
With every request the user makes, I use authentication middleware to make sure there is still user data in the session, so deleting user data in the session object should fail any subsequent authentication.
To test my server, I have a nodeunit test that logs in, calls a few endpoints, logs out, and then attempts to call another endpoint. I would expect the last endpoint to fail in authentication because I previously blew away user data. Instead, when I make the last call, my user data is still there. It's as if the logout call that deleted it was not written back into the session store.
Here's my app.js:
app.use(express.cookieParser('secretPassword'));
app.use(express.cookieSession({key: 'someKey'}));
...
app.get('/logout', accounts.logout);
app.get('/account', auth.authenticateSession, accounts.show);
auth.js:
exports.authenticateSession = function(req, res, next) {
if (!req.session.user) {
return res.json(401, {
error: 'Access denied. You must be logged in to make this request.'
});
}
...
}
accounts.js:logout:
exports.logout = function(req, res) {
req.session.user = null;
res.send('Logged out');
};
Unit tests:
step1_logIn : function(test) {
var postData = qs.stringify({
accountname: 'testAcct',
accountpassword: 'hello'
});
ct.postAndCall('/login', null, postData, function(resData, res) {
myCookie = res.headers['set-cookie'];
test.ok(res.statusCode === 200);
test.done();
});
},
step2_logout : function(test) {
ct.getAndCall('/logout', myCookie, function(data, res) {
test.ok(data === 'Logged out.');
test.ok(res.statusCode === 200);
test.done();
});
},
step3_ensureLoggedOut: function(test) {
ct.getAndCall('/account', myCookie, function(data, res) {
test.ok(res.statusCode === 401);
test.done();
});
}
When the tests run, execution goes through logout successfully, then into authenticateSession on the call to /account and at this point, req.session.user still exists! Why!?
Is my cookie store giving me stale data?
Is there a way to force express to save my session data manually, or do I just modify the req.session object and trust that it's going to save it?
Update:
It looks like this problem is directly related to the app middleware surrounding cookies. When I use app.use(express.session()) instead of app.use(express.cookieSession(...)), the session data is properly blown away and my tests pass.
I figured it out. Apparently express.cookieSession(...) is meant to be a set-once type of storage. Subsequent requests will have access to the session data that was initially set, but changing the req.session object won't save back new session data.
To fix the problem, I switched over to use express.session(...) which uses a server-side store for session vars.

Resources