How should I handle nuxt cookies expiration and workflow? - express

I have made an authentication workflow for a project using a Nuxt frontend(universal mode) and an Apollo endpoint as backend.
It is a mix of several examples I found and, with SSR, and since I do not fully anticipate what could go wrong, I wanted to make sure there is no red flag about how I proceed.
On the backend, I use an express middleware to sign JWT auth tokens, check them, and return them in the Authorization header. Here is the middleware:
import jwt from 'jsonwebtoken';
import { AuthenticationError } from 'apollo-server-express';
export const getToken = payload => {
return jwt.sign(payload, process.env.SEED, { expiresIn: process.env.EXPTOKEN });
}
export const checkToken = (req, res, next) => {
const rawToken = req.headers["authorization"]
if (rawToken) {
try {
const token = rawToken.substring(7)
// Verify that the token is validated
const { user, role } = jwt.verify(token, process.env.SEED);
const newToken = getToken({ user, role });
req.user = user;
req.role = role;
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
res.set("Access-Control-Expose-Headers", "authorization");
res.set("authorization", newToken);
} catch (error) {
if (error.name === "TokenExpiredError") {
res.set("Access-Control-Expose-Headers", "authorization");
res.set("authorization", false);
}
console.log("invalid token", error);
return new AuthenticationError
// Invalid Token
}
}
next();
}
Since there is a Nuxt-Apollo module, I used its methods onLogin, onLogout and getToken to store the JWT string in a cookie. As I understand it, SSR apps don't have the serverside local storage matching the client so they have to use cookies. Correct?
Here is my nuxt middleware where I check the users credentials before allowing them to visit an auth route. Is is quite messy but it gets the job done, except for the commented part.
export default function ({ app, route, error, redirect }) {
const hasToken = !!app.$apolloHelpers.getToken()
// this part does not work
/* const tokenExpireDateTime = app.$cookies.nodeCookie.parse('cookie-name', 'expires')
if (hasToken && tokenExpireDateTime < 0) {
error({ statusCode: 403, message: 'Permission denied', description: 'Sorry, you are forbidden from accessing this page.' })
app.$apolloHelpers.onLogout()
return redirect('/login')
}
*/
if (!hasToken) {
if (route.name === 'welcome-key') {
// enrollment link route
} else {
if (route.name === 'home') {
error({ errorCode: 403, message: 'You are not allowed to see this' })
return redirect('/showcase')
}
if (!['login', 'forgot_password', 'reset_password-key'].includes(route.name)) {
error({ errorCode: 403, message: 'You are not allowed to see this' })
return redirect('/login')
}
}
} else {
if (['login', 'forgot_password', 'reset_password-key'].includes(route.name)) {
redirect('/')
}
}
}
I have one issue and several points of confusion.
My issue is that I can't get the cookie expires value to redirect in the above nuxt middlware if it is necessary to login again because the JWT is expired. I used the piece of code mentioned in this issue as reference.
With this issue, my confusion is about:
The expires date on the cookie is set by the Nuxt-Apollo module, I expect, and I have to make it match the duration set on server (i.e. process.env.EXPTOKEN in the server middleware mentioned above), correct?
That expiration time alone can easily be tempered with and the real security is the lack of a valid token in headers when a request is handled by my server middleware. Its use is for client-side detection and redirect of an expired token/cookie, and serverside prefetch of user related data during SSR. Right?
The new token emitted by my express backend middleware is not taken into account in my frontend: it is not updating the cookie stored JWT and expires value client side. I mean that I can see the autorization header JWT string being updated in the response, but the cookie isn't. The following request still use the first JWT string. Am I supposed to update it at each roundtrip? What am I missing with the approach of the express middleware (that, as you can guess, I didn't write)
Please help me understand better this workflow and how I could improve it. It tried to avoid as much as possible to make this question too broad, but if I can narrow it down more, feel free to suggest an edit.

Related

How to redirect from GraphQL middleware resolver on authentication fail?

Introduction:
I am using GraphQL Mesh as a gateway between my app and an API. I use Apollo Client as the GraphQL client. When a user wants to visit the first screen after hitting the log-in button, I do a query to load data from a CMS. This query has to go through the gateway. In the gateway I do an auth check to see if the user has a valid JTW access token, if not, I want to redirect back to the sign-in page. If the user has a token, he is let through.
The gateway is-auth.ts resolver:
const header = context.headers.authorization;
if (typeof header === "undefined") {
return new Error("Unauthorized: no access token found.");
} else {
const token = header.split(" ")[1];
if (token) {
try {
const user = jwt.verify(token, process.env.JWT_SECRET as string);
} catch (error) {
return new Error("Unauthorized: " + error);
}
} else {
return new Error("Unauthorized: no access token found.");
}
}
return next(root, args, context, info);
},
Problem: Right now, I am returning Errors in the authentication resolver of the gateway, hoping that I could pick them up in the error object that is sent to Apollo Client and then redirect off of that. Unfortunately, I don't get that option, since the Errors are thrown immediately, resulting in an error screen for the user (not what I want). I was hoping this would work in order to redirect to the sign-in from the client-side, but it does not work:
const { data, error } = await apolloClient(accessToken).query({
query: gql`
query {
...where my query is.
}
`,
});
if (error) {
return {
redirect: {
permanent: false,
destination: `/sign-in`,
},
};
}
Does anyone perhaps have a solution to this problem?
This is the GraphQL Mesh documentation on the auth resolver, for anyone that wants to see it: https://www.graphql-mesh.com/docs/transforms/resolvers-composition. Unfortunately, it doesn't say anything about redirects.
Kind regards.

ExpressJS authorization with JWT - Is this correct?

I have a small ExpressJS server with a login feature. Some pages are secured with self-written authenticate middleware that checks if your Json WebToken is correct.
I read that just one Json WebToken isn't secure enough and that you need a refresh token as well. So I added a Refresh Token. This is all verified on the server.
Now when the token is expired, I check if the user has a refreshToken and if so I create a new token and set it as a cookie. Like this:
const jwt = require('jsonwebtoken');
// Simple array holding the issued refreshTokens
const { refreshTokens } = require('../lib/auth.js');
module.exports.authenticateToken = function(req, res, next) {
const token = req.cookies.token;
const refreshToken = req.cookies.refreshToken;
if(token === null) return res.redirect('/login');
try {
const verified = jwt.verify(token, process.env.TOKEN_SECRET);
if(verified) return next();
} catch(err) {
try {
const refreshInDb = refreshTokens.find(token => token === refreshToken);
const refreshVerified = refreshInDb && jwt.verify(refreshToken, process.env.REFRESHTOKEN_SECRET);
const newToken = jwt.sign({ email: refreshVerified.email }, process.env.TOKEN_SECRET, { expiresIn: 20 });
res.cookie('token', newToken, { maxAge: 900000, httpOnly: true });
return next();
} catch(err) {
return res.redirect('/login');
}
}
};
Now is this code correct & secure enough for a small webapplication? Am I missing stuff? It feels so... easy?
Seems like you are veryfing both tokens at the same endpoint. This approach is wrong.
In your login endpoint, validate the user and password against database. If credentials are correct we respond with an access token and a refresh token
router.post('/login',
asyncWrap(async (req, res, next) => {
const { username, password } = req.body
await validateUser(username, password)
return res.json({
access_token: generateAccessToken(), // expires in 1 hour
refresh_token: generateRefreshToken(), // expires in 1 month
})
})
)
In your authenticated routes, you should validate only the access token (the user should send ONLY this one)
// middleware to validate the access token
export const validateToken = asyncWrap(async (req, res, next) => {
const data = await verifyAccessToken(req.headers.authorization)
req.auth = data
next()
})
If the access token expires, the user should refresh its token. In this endpoint we will validate the refresh token and respond with two new tokens:
router.post('/refresh',
asyncWrap(async (req, res, next) => {
const { refresh_token } = req.body
await verifyRefreshToken(refresh_token)
return res.json({
access_token: generateAccessToken(), // expires in 1 hour
refresh_token: generateRefreshToken(), // expires in 1 month
})
})
)
I read that just one Json WebToken isn't secure enough and that you need a refresh token as well. So I added a Refresh Token. This is all verified on the server.
Using refresh tokens has nothing to do with security of the JWT or access token. Refresh tokens are just a UX feature. They allow you to get new access tokens without asking the user to authorize again. Having a refresh token in your app doesn't automatically make it more secure.
Now when the token is expired, I check if the user has a refreshToken and if so I create a new token and set it as a cookie. Like this:
When implemented this way the refresh token doesn't grant any more security to your application. You could as well keep the access tokens in the db and refresh them when they are expired.
Are you sure that you need JWTs at all? It looks like you're using them as you would use a session based on cookies. It should be simpler to deal with sessions. You are using http-only cookies for your tokens so you already use it pretty much like a session.
Now is this code correct & secure enough for a small webapplication?
Secure enough is a concept that depends on the data that your application has access to. If it's nothing sensitive, and you know that your app can't really be abused by an attacker, then it is fine to have only some basic security in place.

Invalid csrf token with NestJS

I would like to implement Csrf protection with NestJS and Quasar.
But I think I misunderstand something...
btw I'm not doing SSR, so I don't send the form from the back to the view.
Here is the NestJs back-end code:
async function bootstrap() {
const PORT = process.env.PORT;
const app = await NestFactory.create(AppModule, {
cors: true,
bodyParser: false,
});
console.log(`your App is listening on port ${PORT}`);
// Added Cookie-parser to user csurf packages
// Prevent CSRF attack
app.use(cookieParser());
app.use(csurf({ cookie: true }));
await app.listen(PORT);
}
bootstrap();
So I'm just using CookieParser and csurf package.
On my login page I call a "csrf endpoint" just to send a cookie to the view, to send it back with the post call (login).
I still get the "invalid csrf token" AND a CORS error and don't know why....(see screen below), any suggestions to make it works ?
When I try to login, error in the browser:
And error in the back-end:
Same error if I try a request with insomnia.
I thought that the CSRF token is attached to the "web browser" to go back to the back-end with nest request, so why I'm still getting this error ?
Insomnia send the cookie automatically with the right request so the token should go back to the back-end.
Any idea ?
Regards
EDIT:
After many times reading docs, It seems that CSRF protection is for SSR only ? No need to add csrf security with SPA ? Could anyone can confirm ?
EDIT: Here's another work:
The purpose here is to send a request before login to get a csrf token that I can put into a cookie to resend when I login with a POST method.
Here is my endpoint:
import { Controller, Get, Req, Res, HttpCode, Query } from "#nestjs/common";
#Controller("csrf")
export class SecurityController {
#Get("")
#HttpCode(200)
async getNewToken(#Req() req, #Res() res) {
const csrfToken = req.csrfToken();
res.send({ csrfToken });
}
}
Here is what I've done into my main.ts file (I'll explain below):
async function bootstrap() {
const PORT = process.env.PORT;
const app = await NestFactory.create(AppModule, {
cors: {
origin: "*",
methods: ["GET,HEAD,OPTIONS,POST,PUT"],
allowedHeaders: [
"Content-Type",
"X-CSRF-TOKEN",
"access-control-allow-methods",
"Access-Control-Allow-Origin",
"access-control-allow-credentials",
"access-control-allow-headers",
],
credentials: true,
},
bodyParser: false,
});
app.use(cookieParser());
app.use(csurf({ cookie: true }));
console.log(`your App is listening on port ${PORT}`);
await app.listen(PORT);
}
bootstrap();
And here my axiosInstance Interceptors of the request in my VueJS frontend:
axiosInstance.interceptors.request.use(
(req) => {
const token = Cookies.get('my_cookie')
if (token) {
req.headers.common['Authorization'] = 'Bearer ' + token.access_token
}
req.headers['Access-Control-Allow-Origin'] = '*'
req.headers['Access-Control-Allow-Credentials'] = 'true'
req.headers['Access-Control-Allow-Methods'] = 'GET,HEAD,OPTIONS,POST,PUT'
req.headers['Access-Control-Allow-Headers'] =
'access-control-allow-credentials,access-control-allow-headers,access-control-allow-methods,access-control-allow-origin,content-type,x-csrf-token'
const csrfToken = Cookies.get('X-CSRF-TOKEN')
if (csrfToken) {
req.headers['X-CSRF-TOKEN'] = csrfToken
console.log(req)
}
return req
},
(err) => {
console.log(err)
},
Here the same for repsonse:
axiosInstance.interceptors.response.use(
(response) => {
if (response?.data?.csrfToken) {
const {
data: { csrfToken },
} = response
Cookies.set('X-CSRF-TOKEN', csrfToken)
}
return response
},
And inside my login I make a call on the mounted function of my login component:
async mounted() {
const result = await securityService.getCsrf()
},
So now to explain:
As I said I'm not building a SSR project, that's why I want to send the token into a classic axios reponse and store it in a Cookie (this part is for test I heard that storing a csrf token into a classic cookie is not the right way.)
And for each next request I get the csrf token and "attach" it to the request into the headers, making my headers "custom".
Here is a problem I don't know how to make custom headers works with nestJS and CORS, that's why I try many thing with CORS options in NestJS and writte some custome header before the request go to the back-end but without success, I've got the same error message:
I'm a bit confuse about this problem and CORS/CSRF is a big deal for spa, my questions still the same, with CORS and SameSite cookie attributes, and my api is in a subdomain of my front-end, is it really necessary to make a anti-csrf pattern ?
Btw how can I make my custom headers working and why CORS say to me there is no "Access-Control-Allow-Origin" header but there is:
try to generate csrf token and pass to front on each petition
// main.ts - from NestJs - Backend
// after app.use(csurf({ cookie: true }))
app.use((req: any, res: any, next: any) => {
const token = req.csrfToken()
res.cookie("XSRF-TOKEN", token)
res.locals.csrfToken = token
next()
})
from: https://github.com/nestjs/nest/issues/6552#issuecomment-1175270849

Express auth middleware not liking the JWT Token it receives from client side React Native AsyncStorage

I'm working on login authentication with a react native app. I have login working as someone logs in for the first time through the login form. At this initial login step, I'm sending my JWT web token to my secure routes in my express server as a string and this works fine. NOTE: I have a middleware setup that my token goes through before my routes will fire completely.
The problem is when the app refreshes it needs to go to my "local storage". My express middleware doesn't like the token I'm pulling from my "local storage (I retrieve it through AsyncStorate.getItem('UserToken'). When I look at the item being stored in the header, it seems like it's the exact item I was sending it at the initial login, but I think it's my middleware on the server that doesn't like the value when it's coming from the " local storage" header. Below is my middlware.js code.
I've tried looking at the JWT value being sent in both scenarios, and it seems like it's the exact item being sent to the server in both situations.
module.exports = function(req, res, next) {
//Get Token from header
const token = req.header('x-auth-token');
// Check if no token
if(!token) {
return res.status(401).json({ msg: 'No token, authorization denied'})
}
//Verify token
try {
var decoded = jwt.verify(token, global.gConfig.jwtSecret);
req.user = decoded.user;
next();
}catch(err){
res.status(401).json({ msg: 'Token is not valid'});
}
}
This is the function I'm using to retrieve the token from AsyncStorage
export const getAsyncStorage = async () => {
try {
// pulling from header to get x-auth-token here
Value = await AsyncStorage.getItem('Usertoken');
//setting x-auth-token here
axios.defaults.headers.common['x-auth-token'] = Value;
} catch (error) {
// Error retrieving data
}
};

Auth0 LoopbackJS API access token using 3rd party login

I currently have the loopbackJS api hosted on a domain (e.g. http://backend.com), with third party authentication setup via Auth0. I have a front-end hosted as a SPA on another domain (e.g. http://frontend.com)
loopback-component-passport seems to work fine when the front-end is on the same domain as the API, and it sets the userId and access_token cookies accordingly. However, my front-end in production is on a different domain to the API, for example the API auth link would be something like:
"http://backend.com/auth/auth0?returnTo=" + encodeURIComponent("http://frontend.com")
The backend has used the same auth pattern as in the loopback-passport-example, where a providers.json file specifies the connection details for Auth0 (although I have also tried other social providers such as Facebook).
"auth0-login": {
"provider": "auth0",
"module": "passport-auth0",
"clientID": "AUTH0_CLIENT_ID",
"clientSecret": "AUTH0_CLIENT_SECRET",
"callbackURL": "/auth/auth0/callback",
"authPath": "/auth/auth0",
"callbackPath": "/auth/auth0/callback",
"successRedirect": "/",
"failureRedirect": "/login",
"scope": ["email"],
"failureFlash": true
}
The front-end (http://frontend.com) has a link on the page to redirect to the API authentication:
Login
Clicking on this link redirects to Auth0 properly, and I can login. It then redirects to the specified target (http://backend.com or http://frontend.com, whichever is specified). The returnTo query parameter also seems to work as expected.
Is there a way to capture the access_token just before redirecting back to the front-end, and somehow communicate it (e.g. query parameters, unless that would be too insecure).
After some more investigation, I settled on this method to use for passing the access token and userId from loopbackjs backend, to a separate front-end. This was documented on a github pull-request, using a customCallback of passport-configurator.
Other places that have referenced this are this fork, issue #102, issue #14 and pull request #155.
There are 2 options here, either use a fork of loopback-component-passport (e.g. the one referenced above) as your npm dependency, or provide a customCallback as a passport configuration option as documented.
I wanted a little more control on the format of the URL, so ended up with the customCallback method. In loopback-example-passport, inside /server/server.js there is some basic code for passing providers.json to the passport configurator:
var config = {};
try {
config = require('../providers.json');
} catch (err) {
console.trace(err);
process.exit(1); // fatal
}
passportConfigurator.init();
for (var s in config) {
var c = config[s];
c.session = c.session !== false;
passportConfigurator.configureProvider(s, c);
}
This can be essentially replaced with the documented customCallback code, with the passport variable being assigned by passportConfigurator.init():
var providers = {};
try {
providers = require('../providers.json');
} catch (err) {
console.trace(err);
process.exit(1); // fatal
}
const passport = passportConfigurator.init();
Object.keys(providers).forEach(function(strategy) {
var options = providers[strategy];
options.session = options.session !== false;
var successRedirect = function(req) {
if (!!req && req.session && req.session.returnTo) {
var returnTo = req.session.returnTo;
delete req.session.returnTo;
return returnTo;
}
return options.successRedirect || '';
};
options.customCallback = !options.redirectWithToken
? null
: function (req, res, next) {
var url = require('url');
passport.authenticate(
strategy,
{session: false},
function(err, user, info) {
if (err) {
return next(err);
}
if (!user) {
return res.redirect(options.failureRedirect);
}
var redirect = url.parse(successRedirect(req), true);
delete redirect.search;
redirect.query = {
'access_token': info.accessToken.id,
'userId': user.id.toString()
};
redirect = url.format(redirect);
return res.redirect(redirect);
}
)(req, res, next);
};
passportConfigurator.configureProvider(strategy, options);
});
In the above example, I have essentially copied the successRedirect function used in passport-configurator.js, to use the same returnTo query parameter. An option within providers.json can be set e.g. "redirectWithToken": true, which results in redirect only for the auth strategies that need external redirect.
One more final bit of code in case the returnTo redirect is required. If it exists as a query parameter, it should be added at a session level:
app.use(function(req, res, next) {
var returnTo = req.query.returnTo;
if (returnTo) {
req.session = req.session || {};
req.session.returnTo = require('querystring').unescape(returnTo);
}
next();
});
Now, if the backend api is at a URL such as http://api.com, and the front-end is hosted at another domain e.g. http://gui.com, an authentication link can be placed on the front-end:
Login!
This will result in an API auth call, then redirect back to the returnTo link with the access token and userId in the query parameters.
Potentially in the future, one of the issues or other pull requests will be merged that could provide a more ideal method for 3rd party domain redirection, but until then this method work well.