JWT Authentication with React/Node - node.js

I am facing a 'strange' behavior while trying to implement an authentication using JWT httpOnly in a Node/React application.
Basically I have a "Login" service that would provide a response containing a httpOnly access token if the user and password are correct. If so, the response will be as follows:
(NODEjs - Login snippet)
res.cookie(
"access_token",
{ token: token },
{
maxAge: 3600,
httpOnly: true
}
)
That means that from now on I can only call my APIs by using this web token in this case using parameter withCredentials = true
(Frontend Snippet)
handleTokenVerification(event){
axios.post('/auth/getUserInfo', {withCredentials: true})
.then(function(data) {
console.log(data)
}).catch(err =>{
console.log('ERRO: ' + err)
})
event.preventDefault()
}
It does work when a call a API for the first time, after that, it seems that this cookie was reset some how because de API does not work anymore.

The problem was in the maxAge: 3600 parameter. Was too low and I was not able to perform a couple requests before it expired.

Related

How to set a cookie with Axios and ExpressJS

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.

Trying and failing to send an httponly cookie back to my refresh token endpoint using Jest and Supertest; is it even possible?

A coworker told me "httponly cookies cant be interacted with by the browser, hence you can't integration test anything with them." But someone in a Discord server told me otherwise: "httpOnly only means something to a browser. Jest won't care about it." I am unsure of this so I am testing it.
My goal is to send the httponly refresh token I receive from the login endpoint back to the "refresh the refresh token" endpoint, and receive a refreshed refresh token.
My expected result is that the refresh token endpoint will console log the token. My actual result is that the value is undefined.
The tech is Jest, Supertest, and an Express server.
Here is my code in the integration test suite:
import request from "supertest";
const api = request(server);
// later in a test ...
const loginResponse = await api.post("/auth/email/login").send({ email: signupPayload.email });
// loginResponse is a success.
const rt1 = loginResponse.headers["set-cookie"][0].split("=")[1].split(";")[0];
const x = loginResponse.headers["set-cookie"][0];
// can console log the refresh token cookie and its value here
const refreshResponse = await api.post("/auth/jwt/refresh").set("refreshToken", x).send({});
Neither the "rt1" or "x" value works for the .set("refreshToken", arg) method. Both are undefined.
In my controller:
// where I set the refresh token: this is in a controller
public async emailLogin(req: Request, res: Response) {
// snip
const cookieOptions = {
httpOnly: true,
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
};
res.cookie("refreshToken", newRefreshToken, cookieOptions);
return res.status(200).json({ member, accessToken });
}
public async refreshJwt(req: Request, res: Response) {
try {
const token = req.cookies.refreshToken;
console.log(token, "refresh token"); // value is undefined!
// ... continued
} catch (err) {
// handle err
}
}
Have I been mislead? Is it impossible to use Jest and Supertest to integration test a backend that has an httponly refresh token cookie? or am I doing something wrong?
edit: I can console log the refresh token cookie's value: refreshToken=a511d8a55839ee6a7d6102b3c0a184a432de160a3bd0e8a2c679e5fed272f3cac87eb17cfbff5af3; Path=/; Expires=Thu, 29 Dec 2022 23:33:24 GMT; HttpOnly 48rm
This seems to indicate I can send it back properly. Yet I fail.
I'm a newb, the solution was to replace .set("refreshToken", x) with .set("Cookie", [x]) and it works! Found my answer by reading the docs, of course. https://www.npmjs.com/package/supertest <= ctrl + f .set(
Less messy solution:
const refreshToken1 = loginResponse.headers["set-cookie"];
const refreshResponse = await api.post("/auth/jwt/refresh").set("Cookie", refreshToken1);
The more important part is that yes, you can send back an http only cookie using supertest, and view it on the server. Hence anyone else wondering if its doable can find this post and have their answer

using refresh token to get new access token react and node js

I've built a JWT API for user authentication. I want to use it with react.
my app has these routes:
Node js API Routes:
http://localhost:8080/api/auth/signin -> this route accepts username and password from react and sends back an access token to react which it will save it inside localstorage(I'm planning to use memory instead) and cookie containing refresh token with httpOnly enabled.
http://localhost:8080/api/auth/signup -> this route will add a new user to database based on user input(doesn't relate to authentication process)
http://localhost:8080/api/auth/refreshtoken -> this route will return a new access token based on the sent refresh token from client which was saved inside cookies. and then client will replace it with the expired access token.
http://localhost:8080/api/test/user -> this will check if user is signed in with the access token from client and will send the user data back
React Client Routes:
http://localhost:3000/login -> a route for sending user information for logging in
http://localhost:3000/register -> a route for creating a new user
http://localhost:3000/user -> a protected route which needs user to be logged in to view data.
when I log in to my website I can see the data inside the user route for 20 seconds before the access token expiry. when the access token is expired and I do a new request the server will response with 401 unauthorized code. so this way I can make sure that the access token is expired and I need to use refresh token. I've done that like this:
const API_URL = "http://localhost:8080/api/test/";
const getUserBoard = () => {
return axios.get(API_URL + "user", { headers: authHeader() })
.catch((error) => {
if(error.response.status === 401) {
// if error response status was 401 then request a new token
authService.refreshToken();
window.location.reload();
}else if(error.response.status === 403) {
authService.logout();
window.location.href = "/login";
}
});
};
code to get new access token with refresh token:
const refreshToken = () => {
axios({
url: API_URL + "refreshtoken",
method: "POST",
withCredentials: true
})
.then((response) => {
if(response.data.accessToken) {
const user = JSON.parse(localStorage.getItem("user"));
user.accessToken = response.data.accessToken;
localStorage.setItem("user", JSON.stringify(user));
}
})
.catch((error) => {
console.log("server: " + JSON.stringify(error.response));
})
}
code to set header for receiving data from server using access token:
export default function authHeader() {
const user = JSON.parse(localStorage.getItem('user'));
if (user && user.accessToken) {
// for Node.js Express back-end
return { 'x-access-token': user.accessToken };
} else {
return {};
}
}
the first part for getting new access token on 401 error works good but my refresh token also expires after 40 second. if I send the expired refresh token it won't return a new access token. and because of that again I will receive a 401 error which again will cause a 403 error and here I'll get stuck in an infinite reload loop.
any idea? how can I control tokens expiry?
You're issuing tokens in your node.js app, right? So that is where you should adjust the expiration time of the token. The code which issue tokens should have an option to set the expiration time.
Remember that once the refresh token is expired you should log in again. You can implement something which is called a rolling refresh token. So whenever you call the /api/auth/refreshtoken endpoint you can also issue a new refresh token, with a new expiration time and return it in a cookie.

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.

Web and mobile, where to store token/cookies

I'm developing a program which consists of a back-end server, a mobile application, and a web application. I have added JWT token to my mobile application and I'm storing in async storage. However, I cannot find an appropriate way to do in web server.
I have created a middleware file for creating and checking token's validity. In my API route's I'm doing the following
router.post('/:url', middleware.checkToken, (req, res, next)=>
{
...
}
So, every time I call this API, middleware file checks for the token. In my mobile application, I'm storing the token in the async storage and pass it to the web server.
However, in the browser side, I want to store the token inside a cookie rather than storing in local storage. How can I do this without changing my code?
This is mobile login API.
router.post('/login', (req,res,next) => {
let username = req.body.username;
let password = req.body.password;
User.findOne({'username' : username})
.exec()
.then(doc =>{
if(doc.validPassword(password))
{
let token = jwt.sign({
id: doc.id,
email: doc.email,
},
config.secret,
{ expiresIn: '24h' // expires in 24 hours
}
);
res.status(200).json({
success: true,
message: 'Authentication successful!',
token: token
});
}
else{
// invalid credentials
res.send(403).json({
success: false,
message: 'Incorrect username or password'
});
}
})
})
I don't want a separate file for web login. I just want to use the same code, without copying to another file.
Should I write another different code for both mobile and web but one send a cookie, other sends plain token? Is there any way to achieve this with simple solution?
In short:
Mobile users send credentials to the mobile login page and they receive token.
Web users send credentials to the web page and they receive a cookie (a token resides inside the cookie). I don't want to have separate code for login.
You can easily add a new cookie in the front-end with document.cookie = ... (MDN document cookie)
In your middleware, you just have to parse for cookie instead of some Bearer token or whatever.

Resources