How to set a cookie with Axios and ExpressJS - node.js

I want to set a cookie after logging in. This is my log in method:
(The res.cookie stuff doesn't work)
router.post("/login", (req, res) => {
const userForToken = { username: req.body.username };
const token = auth.generateAccessToken(userForToken);
axios
.post(userAPI + req.path, req.body)
.then(() => {
res.cookie("talloc_user_cookie_token", token, {
maxAge: 60 * 60 * 24 * 7,
httpOnly: true,
});
res.send({
username: req.body.username,
token: token,
});
console.log(
`✅ Succesfully logged as <<${req.body.username}>> Status Code: ${res.statusCode}`
);
})
.catch(() => {
res.sendStatus(404);
console.log("⛔ Error logging in. Status Code: ", res.statusCode);
});
});
I have been reading other questions and forums and everybody says to use { withCredentials: true }, which in my case just gets the request to not work and just catch the error. I have also saw that I'd need to add the Access-Control-Allow-Origin header in the request, but It doesn't seem to work too.
For anyone wondering, I am trying to make a double token submit, where I have a JWT token set in localStorage, and a cookie that also has that token, so whenever my app's middleware authenticates the user, it has to check whether the token is correct and also if the token in localStorage is equals to the cookie's token, because cookie's values cannot be changed, whereas in localStorage it is in fact changable.

Related

JWT authentication working in postman but authentication token not being set to cookies in browser

I am using JWT to set a token for user authentication on specific routes. The authentication works perfectly with the Postman routes, but when I log in using the browser and the app sends the GET requests to my Heroku backend, I get a 401 Unauthorized error. I am using the same Heroku URL when sending a postman request, so I am sure that my Heroku hosting is not the issue.
The flow of the app is as follows:
Once a user tries to log in, we check credentials with the DB and if the credentials are correct, we then assign a jwt token to the cookie. This occurs on the server side using the encode function below.
export const encode = async (req, res, next) => {
try {
const { username, password } = req.params;
const verifiedLogin = await User.onUserLogin(username, password);
if (verifiedLogin.success === false) {
return res.status(401).json({ success: false, message: "Invalid login credentials" });
}
const token = jwt.sign({ userid: verifiedLogin.user._id, userType: verifiedLogin.user.type }, SECRET_KEY);
res.cookie("Authorization", token, { httpOnly: false, secure: false });
console.log("cookie", res.cookie);
return res.status(200).json({ success: true, userId: verifiedLogin.user._id });
} catch (error) {
return res.status(400).json({
success: false,
message: "Problem while trying to authenticate ",
error: error,
});
}
};
Once the user is logged in, we then run a series of GET requests from the client side to retrieve some data from our DB. None of the GET requests are authorized after login so I will only provide one of them here.
export const fetchUserGroups = async () => {
let roomsFromResponse = [];
// Request to get the groups the user is part of for the groups panel
await axios
.get(`http://saldanaj97-chattyio.herokuapp.com/room/user-messages/`, CONFIG)
.then((response) => {
response.data.roomIds.map((room) => {
const newRoom = { id: room._id, groupName: room.groupName, lastMessageReceived: { user: "", contents: "" } };
return (roomsFromResponse = [newRoom, ...roomsFromResponse]);
});
})
.catch((error) => {
console.log("Auth error when retrieving users groups", error);
});
return roomsFromResponse;
};
Lastly, when a user is trying to access a route that requires authentication(such as the one above), the decode middleware below is used to decode the jwt token.
export const decode = (req, res, next) => {
if (req.cookies === "") {
return res.status(400).json({ success: false, error: "No access token provided " });
}
const accessToken = req.cookies["Authorization"];
try {
const decoded = jwt.verify(accessToken, SECRET_KEY);
req.userId = decoded.userid;
req.userType = decoded.userType;
return next();
} catch (error) {
return res.status(401).json({
success: false,
message: "Could not decode authorization token",
});
}
};
Below are my cors settings from the backend.
// Cors
const corsOptions = {
origin: ["http://localhost:3000"],
exposedHeaders: ["Authorization"],
};
app.use(cors(corsOptions));
app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content, Accept, Content-Type, Authorization");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
res.setHeader("Access-Control-Allow-Credentials", true);
next();
});
When a user logs in, an error is caught and the error is posted in the console with a 401 code along with the 'could not decode authorization token' from the decode function catch block from above. When I go to check the application settings in chrome, there is no Authorization token being set after login, but when I run the same GET request in postman, the cookies ARE being set.
I have tried different things such as settings 'httpOnly' to true as well as setting 'secure' to true but neither has worked. This is my first app that I have tried hosting and every feature was working fine while developing in localhost but once I put it on Heroku I have not been able to get the authorization to work in the browser even though all my requests from postman continue working.
I am not sure if this is a cors issue, an issue with the decode function, or a different issue altogether. Any help would be appreciated.
If you're not on the same domain, you can't set cookie for the client from server, it works in postman since you're hitting the endpoint from the same domain.
https://stackoverflow.com/a/44516536/12490386

Sending a cookie as a response with Firebase Callable Functions

I am trying to send a cookie with options set to it as a response using a Firebase callable cloud function (https.onCall). I see in the Firebase docs that this can be done with express:
(The below is taken directly form the Firebase docs)
app.post('/sessionLogin', (req, res) => {
// Get the ID token passed and the CSRF token.
const idToken = req.body.idToken.toString();
const csrfToken = req.body.csrfToken.toString();
// Guard against CSRF attacks.
if (csrfToken !== req.cookies.csrfToken) {
res.status(401).send('UNAUTHORIZED REQUEST!');
return;
}
// Set session expiration to 5 days.
const expiresIn = 60 * 60 * 24 * 5 * 1000;
// Create the session cookie. This will also verify the ID token in the process.
// The session cookie will have the same claims as the ID token.
// To only allow session cookie setting on recent sign-in, auth_time in ID token
// can be checked to ensure user was recently signed in before creating a session cookie.
getAuth()
.createSessionCookie(idToken, { expiresIn })
.then(
(sessionCookie) => {
// Set cookie policy for session cookie.
const options = { maxAge: expiresIn, httpOnly: true, secure: true };
res.cookie('session', sessionCookie, options);
res.end(JSON.stringify({ status: 'success' }));
},
(error) => {
res.status(401).send('UNAUTHORIZED REQUEST!');
}
);
});
I have implemented the callable function, but I do now know how to attach the options to my cookie string.
The below is my code:
// I want the return type to be a Promise of a cookie object, not a string
export const setCookie = https.onCall(async (context: https.CallableContext): Promise<string> => {
try {
console.log(context);
const auth: Auth = getAuth();
const idToken: DecodedIdToken = await auth.verifyIdToken(context.instanceIdToken!); // https://firebase.google.com/docs/auth/admin/verify-id-tokens#web
console.log("idToken: ", idToken);
const cookie: string = await auth.createSessionCookie(idToken.uid, { expiresIn: 300000 });
const options = {
maxAge: 300000,
httpOnly: true,
secure: true,
sameSite: "strict",
};
// res.cookie("session", cookie, options);
return cookie; // should be assigned to __session cookie with domain .web.app
// httpOnly=true, secure=true and sameSite=strict set.
} catch (error) {
console.log("ERROR FOUND: ", error);
throw new https.HttpsError("unknown", "Error found in setCookie");
}
});
Is there any way I can do this using a Callable Firebase Cloud Function? All the documentation and resources I have found require express to send an cookie with Node.
Thanks!
The documentation you're linking to assumes you are writing standard nodejs backend code using express. However, your code is using a callable type function. They are not the same and do not have the same capabilities. Callable functions don't let you set cookies in the response. You can only send a JSON payload back to the client; the SDK handles all of the HTTP headers and they are outside of your control.
Perhaps you should look into using a standard HTTP type function (onRequest), where you do have some control over the headers in the response.

How to parse signedCookies in express?

I send a cookie to my front-end thanks to express:
// signing the token
static generateToken(user: PartialUser): Cookie {
return jwt.sign({ _id: user._id }, process.env.JWT_TOKEN_KEY, {
expiresIn: "14d",
});
// sending the cookie
return res
.status(200)
.cookie("myApp", token, {
expires: new Date(Date.now() + msPerDay * 14),
httpOnly: true,
secure: true,
})
.json({ user });
// initializing cookie parser in index.js:
app.use(cookieParser(process.env.JWT_TOKEN_KEY));
//parsing the cookie
const authenticate = (req: Authenticate, res: Response, next: NextFunction) => {
const { myApp } = req.signedCookies;
if (req.signedCookies) {
return jwt.verify(
myApp,
process.env.JWT_TOKEN_KEY,
(error, parsedToken) => {
if (error) {
return res.sendStatus(403);
}
req.cookie = { _id: parsedToken._id };
return next();
}
);
}
return res.sendStatus(401);
};
The req.signedCookies object is always empty. So all my routes return a 403 - forbidden access. However, if I don't specify secure: true when sending the cookie, it works, because I can access it in req.cookies. The network tab shows that the cookie is correctly send along the client request.
How to fix this?
ps: I'm fine with using req.cookies, but my express server is hosted on Heroku and it never sends the cookie to the client, which is a custom https domain. This is why I'm trying the secure:true option. For now, it only works in localhost. Maybe the solution is elsewhere?
A cookie signature on one hand, and the secure option on the other hand, are actually two different things.
The secure option restricts the cookie to be sent over https only. This is intended at avoiding eavesdropping over the network. Incoming cookies that are set as secure will by default always be exposed on req.cookies by cookie-parser.
A cookie signature on the other hand is basically a cryptographic hash that is intended at making a cookie tamper-proof. It seems that with the cookie-parser package, you sign a cookie with the signed: true option. Only incoming cookies that have been explicitly signed will be exposed on req.signedCookies. Note this is all regardless of the secure option.

How to secure JWT token and refresh token in nodejs/angular

i am new in node js. I am building a simple notes taking app and wanted to use JWT tokens for authentication and to secure my API's. On research i came to know that i need to create two tokens:
access token (short expire time like 10 minutes)
refresh token (longer expire time 30 days)
My config file
"secret": "*************",
"refreshTokenSecret": "*************",
"port": 5000,
"tokenLife": 900,
"refreshTokenLife": 86400
Code for middleware
const jwt = require('jsonwebtoken')
const config = require('./config')
module.exports = (req,res,next) => {
const token = req.body.token || req.query.token || req.headers['x-access-token']
// decode token
if (token) {
// verifies secret and checks exp
jwt.verify(token, config.secret, function(err, decoded) {
if (err) {
return res.status(401).json({"error": true, "message": 'Unauthorized access.' });
}
req.decoded = decoded;
next();
});
} else {
// if there is no token
// return an error
return res.status(403).send({
"error": true,
"message": 'No token provided.'
});
}
}
Here is the response
access token can be saved in local storage. but articles said save refresh token as http-only cookie.
i need the answer of following points (Keeping in mind that i am just a beginner):
How to store refresh token as http-only cookie (any node-js code
example would be a great help)?
How to secure it on client side and should I save refresh token to database?
Is there any other better solution to secure my API's?
You can use an http-only cookie using the following:
public authenticateUser(user: User, res: Response) {
const authJwtToken = this.generateJWT({
email: user.email,
uuid: user.uuid
});
const cookieOptions = {
maxAge: 3600000,
secure: true,
httpOnly: true
};
res.cookie('access_token', authJwtToken, cookieOptions);
}
// you can then res.send({...}) or wtv
Not that there is nothing from preventing you to store more than one cookie so I can't see a reason why not storing both of them in the same manner.
Now whether you will store it on the database depends on what you want to achieve.
Generally it is not required but note that in that case the server cannot in any way invalidate a single JWT. (You could in theory change the signing key but this would invalidate all of them).
In case you want to be able to achieve functionality such as 'log me out of all devices' you would need to store the JWTs issued for each user in a database (preferably an in-memory one such as Redis or Memcached) and do a second check with the extra information on whether they have been invalidated or not - even though such functionality is typically achieved using sessions instead of JWT
See this example how i secured my getByRefId api in nodjs :
In routes file :
router.get("/refId/:refId", helper.auth, groupController.getByRefId);
helper.auth is function :
auth: (req, res, next) => {
var token = req.body.token || req.headers['authorization'] || req.headers['Authorization'];
if (token.startsWith('Bearer ')) {
// Remove Bearer from string
token = token.slice(7, token.length).trimLeft();
}
if (token) {
jwt.verify(token, 'MY_SECRET', function (err, decoded) {
if (err) {
console.error('JWT Verification Error', err);
return res.status(403).send(err);
} else {
req.payload = decoded;
return next();
}
});
} else {
res.status(403).send('Token not provided');
}
}
This use jwt = require('jsonwebtoken') library you can install it in nodejs project

Passport & JWT & Google Strategy - Disable session & res.send() after google callback

Using: passport-google-oauth2.
I want to use JWT with Google login - for that I need to disable session and somehow pass the user model back to client.
All the examples are using google callback that magically redirect to '/'.
How do I:
1. Disable session while using passport-google-oauth2.
2. res.send() user to client after google authentication.
Feel free to suggest alternatives if I'm not on the right direction.
Manage to overcome this with some insights:
1. disable session in express - just remove the middleware of the session
// app.use(session({secret: config.secret}))
2. when using Google authentication what actually happens is that there is a redirection to google login page and if login is successful it redirect you back with the url have you provided.
This actually mean that once google call your callback you cannot do res.send(token, user) - its simply does not work (anyone can elaborate why?). So you are force to do a redirect to the client by doing res.redirect("/").
But the whole purpose is to pass the token so you can also do res.redirect("/?token=" + token).
app.get( '/auth/google/callback',
passport.authenticate('google', {
//successRedirect: '/',
failureRedirect: '/'
, session: false
}),
function(req, res) {
var token = AuthService.encode(req.user);
res.redirect("/home?token=" + token);
});
But how the client will get the user entity?
So you can also pass the user in the same way but it didn't felt right for me (passing the whole user entity in the parameter list...).
So what I did is make the client use the token and retrieve the user.
function handleNewToken(token) {
if (!token)
return;
localStorageService.set('token', token);
// Fetch activeUser
$http.get("/api/authenticate/" + token)
.then(function (result) {
setActiveUser(result.data);
});
}
Which mean another http request - This make me think that maybe I didnt get right the token concept.
Feel free to enlighten me.
Initialize passport in index.js:
app.use(passport.initialize());
In your passport.js file:
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL:
'http://localhost:3000/auth/google/redirect',
},
async (accessToken, refreshToken, profile,
callback) => {
// Extract email from profile
const email = profile.emails![0].value;
if (!email) {
throw new BadRequestError('Login failed');
}
// Check if user already exist in database
const existingUser = await User.findOne({ email
});
if (existingUser) {
// Generate JWT
const jwt = jwt.sign(
{ id: existingUser.id },
process.env.JWT_KEY,
{ expiresIn: '10m' }
);
// Update existing user
existingUser.token = jwt
await existingUser.save();
return callback(null, existingUser);
} else {
// Build a new User
const user = User.build({
email,
googleId: profile.id,
token?: undefined
});
// Generate JWT for new user
const jwt = jwt.sign(
{ id: user.id },
process.env.JWT_KEY,
{ expiresIn: '10m' }
);
// Update new user
user.token = jwt;
await auth.save();
return callback(null, auth);
}
}));
Receive this JWT in route via req.user
app.get('/google/redirect', passport.authenticate('google',
{failureRedirect: '/api/relogin', session: false}), (req, res) => {
// Fetch JWT from req.user
const jwt = req.user.token;
req.session = {jwt}
// Successful authentication, redirect home
res.status(200).redirect('/home');
}

Resources