extract the expiration datetime from jsonwebtoken - javascript

To invalidate a token it's as far as I know the best way to store the token and it's expiration datetime to the database. To validate it, you simply have to select it from the database and if it exists, you know it was invalidated. Further you can remove every expired token by it's expiration datetime from the database.
So I created a middleware that extracts the token from the authorization headers and it should attach the token and the expiration datetime to the request object. The datetime is required for the signOut route to invalidate the token.
async use(req: any, res: Response, next: NextFunction) {
try {
const headers: IncomingHttpHeaders = req.headers;
const authorization: string = headers.authorization;
const bearerToken: string[] = authorization.split(' ');
const token: string = bearerToken[1];
if (await this.authenticationsRepository.findByEncodedToken(token)) { // invalidated token?
throw new Error(); // jump to catch
}
req.tokenPayload = verifyToken(token); // calls jwt.verify with secret
next();
} catch (error) {
throw new UnauthorizedException();
}
}
But how can I extract the exp attribute from the token to calculate the expiration date time?

In order to get expiration date you need to decode the jsonwebtoken and access it's exp key, kind of like this:
let token = jwt.sign({
data: 'foobar'
}, 'secret', { expiresIn: '1h' });
var decoded = jwt.decode(token, { complete: true });
console.log(decoded.payload.exp);
In your case you can do it like this I think:
req.expirationTime = jwt.decode(token, { complete: true }).payload.exp;

Related

jwt.verify keeps on returning invalid signature despite working elsewhere

I have no idea why, but for some reason the jwt.verify function is complaining about an invalid signature in only part of my application. To give you some background, I am trying to set up auth headers using the context function with Apollo Server (which I guess is irrelevant anyway as the jwt.verify function should work in any javascript code snippet the same way) as follows:
context: ({ req }) => {
const token = req.get('Authorization') || '';
if (!token) {
console.log('no token detected. returning null...');
return { user: null };
}
return {
user: verifyUser(token.split(' ')[1]),
};
},
The verifyUser function:
const verifyUser = (token: string) => {
try {
return jwt.verify(token, process.env.JWT_SECRET as Secret);
} catch (error: any) {
console.error('error: ', error.message);
return null;
}
};
Note that I’m following this guide and have renamed getUser to verifyUser.
In each graphql request, I am providing a Bearer token, like so:
{
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwczovL2F3ZXNvbWVhcGkuY29tL2dyYXBocWwiOnsicm9sZXMiOlsiYWRtaW4iXSwicGVybWlzc2lvbnMiOlsicmVhZDphbnlfYWNjb3VudCIsInJlYWQ6b3duX2FjY291bnQiXX0sImlhdCI6MTU4NjkwMDI1MSwiZXhwIjoxNTg2OTg2NjUxLCJzdWIiOiIxMjM0NSJ9.31EOrcKYTsg4ro8511bV5nVEyztOBF_4Hqe0_P5lPps"
}
But every time I make a graphql request (a query or mutation) in the graphql playground, I am getting an invalid token message in the catch block, so presumably the jwt.verify function is failing. I wondered whether the JWT I provided to the Bearer code above is wrong, but I do not think it is. I am getting it from an authenticateUser resolver:
export const authenticateUser = async (
_: undefined,
{ input: { email, password } }: any
): Promise<any> => {
try {
const user = await User.findByEmail(email);
if (!user) {
throw new HttpError(404, 'User not found. Please create an account.');
}
const correctPassword = await bcrypt.compare(password, user.hashedPassword);
if (!correctPassword) {
throw new HttpError(403, 'Wrong password for this account.');
}
// assign object methods to the user instance as objects retrieved from db don't have methods
Object.setPrototypeOf(user, User.prototype);
const token = user.signToken(); // see below
return { token, id: user._id };
} catch (error: any) {
throw new HttpError(error.statusCode, error.message);
}
}
The user.signToken() function comes from a User class:
signToken(expiry: number | string = 60 * 24): string {
const secret = process.env.JWT_SECRET + this.hashedPassword;
// { ...this } overcomes error `Expected "payload" to be a plain object`
const token = jwt.sign({ ...this }, secret, {
expiresIn: expiry,
});
return token;
}
The only difference is that I am passing in a hashed password with the secret argument. I’ve also noticed that the error does not occur when using jwt.sign instead of jwt.verify (like in this post), but I assume I have to use the verify function.
Does anyone know why my code may not be working? The guide I am following does not pass a hashed password into the secret argument.

How to find a Forum or a Node by passing a JWT Token?

I would like to know how I can find my forum using my JWT token
exports.getByOwnerID = function (req, res, next) {
Forum.find({createdBy: req.body.createdBy})
.then(doc => {
if(!doc) { return res.status(400).end();}
return res.status(200).json(doc);
})
.catch(err => next(err));
}
So here I have my function to verify my JWT Token
I use it for example this way
this is my route :
router.post('/',verifyToken,getOwner);
this is my request :
POST http://localhost:8080/forum/getOwner
Authorization: Bearer {token}
const extractToken = (rawTokenHeader) => {
if(!rawTokenHeader) { return undefined; }
// Remove bearer and extract token value
const temp = rawTokenHeader.split(' ');
if(!temp || temp.length != 2) { return undefined; }
// Return encoded token
return temp[1];
};
module.exports = function(req,res,next){
// Get authorization header
const rawTokenHeader = req.header('Authorization');
// Get token value
const token = extractToken(rawTokenHeader);
// No token -> No access
if(!token) {
console.log('No token in request');
// Access denied
return res.status(401).send('Access Denied');
}
// Verify token
try {
const decoded = jwt.verify(token, process.env.JWT_KEY);
req.token= decoded;
req.user = decoded;
//console.log(token.userID);
// Proceed
next();
} catch(err) {
console.error('Error in JWT check: ', err);
// Tell client something went wrong
res.status(400).send('Invalid Token');
}
}
const forumSchema = ({
forumName: {
type: String,
required: true,
},
forumDescription: {
type: String,
required: true,
},
createdBy: {
type: Schema.Types.ObjectId, ref: 'User'
},
published_on: {
type: String,
default: moment().format("LLL")
},
});
I´ve tried a lot of things but I can´t solve it anymore.. I need help
How I don't have enough reputation to make a comment I leave this as an answer, is hard to say why this is not working if we don't know how the schema of Forum looks like and what is returning req.body.createdBy, but if the jwt is created by you could encode the Forum._id in it and when you receive it here you can decode it and find the forum in the database
edit---
Now that I can see the error you got and the Schema of Forum i can say that probably you can solve the error by importing mongoose and adding this at the query mongoose.Types.ObjectId(req.body.CreatedBy) that will parse the string to an objectId
As you can see, you have user (or token) data in the req object, and I hope your jwt token also includes the user's id. You can use the user id to find their Forums.
According to the router router.post('/', verifyToken, getOwner); (getByOwnerID ???), let's update getOwner handler:
exports.getOwner = function (req, res, next) {
Forum.find({ createdBy: req.user.userID }) // or something like that
.then(doc => {
if(!doc) { return res.status(400).end();}
return res.status(200).json(doc);
})
.catch(err => next(err));
}

Axios interceptor expiration time

Below I will post my AXIOS code. What happens first when I start the application (Vue js) is that it opens login page and when I enter username/password I set token and refresh token in local storage. That works fine and I can make other api calls which require token. The problem happens when it expires (in 3 minutes after login), it goes into infinite loop. Also the thing is I don't know how to check expiration time of refresh token because I can't decode it with this function I wrote.
Access token:
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIyIiwianRpIjoiMTg4YjQ0NDg2MzdmMmJhYWRkMDE1MmU5OWRhNGIwMWYxMzgxMzVjY2Q5YjA2NmRlM2M4YjFkZjk4ODE1ZGZmNGZhOGE2ODQ2YWI4ZjY1YjkiLCJpYXQiOjE2MjE1MzcxOTAuNjg5MjgzLCJuYmYiOjE2MjE1MzcxOTAuNjg5Mjg2LCJleHAiOjE2MjE1MzczNzAuNjc3NDM3LCJzdWIiOiIyIiwic2NvcGVzIjpbXX0.U5lNHetMq6vEnUKwxlJ9sa9lU6ahj-lDlxjWFdaTuXaGCcmx8zb917OSKkZa1g8PA3NArC6nMVbWfbD44DXLF3I6UFFXAYNncuH8kAngIh-XyRhUgr3MDOR04dCb02Khchs30QnbznHFvox1wtTXLEIT2wzdGI0_GGQot3ZFvxBfukRVt64uqC7GrVxcpoZXV2LXY7LxkZXoEd88QFcjfWWw_RC1fyU7gNaGxF4xml5CyJGZOcM1S-1QlBsXE-HE5qeJPZilxOJLHvxSYo-HFTbl7u0WNlryyCAxJqoeMHIqmHrEmZX261IdMFdQ7sl9YP-rXtg5hY_SDVoaE-KjHThltKvPkV_XeWxWQ3KqCDqm7UMZyxkWzEMglE4Ym8hvNsgUIlZMVeKCuYkQ2Vri-X2whttaVwM4-pJPbAqJURYu2WRDWgBbIWWkXkw4GLUFTDIllOmIBESUjba_L3x2dHrce3PpBOBw8dYDPttdqch6t_J7vBsRUu8-DcHDzxnVu6vBYmQA-TAlI9yN7gOgn_gMDMq6FhitKuQ9KghACJmTjqB-_BbxAI3pWwAuPeAas7uB9ugzpScKPPZtThoI08wQ8pT7Xz8JvZTEharzUHcldu2rIlUCif6l-rtszIQNYcCfWFMBVP9HFRSgCcEtgl3L5SPfQGW0Ytc2P_ED4HE
Refresh token:
def502009a8ebae0e2d2d18b541daa39725ed826115ce9612db43e399b3edc7fe7d08950e5972b8c13faa8846962fb4a027a95a5cdefcdb526051644a1031c909ee6ad1cbc421aa12d0a096728dff99e8c72aef8e7e527824287274cee8d702d20e7468985d5d648c990df99990c283b490bb33d97cf2ecfaf176e6ecd2259db183d95d7bdd664600319d6af36e463e777e01cdd364cdbf146d10ea9a58a1ba5b01adb98ac7ffb27ebfd10cb62f79d1bcb7bf13c3adc1fe70f9de554bc98258ac8071d46a1cc51812140fae06291868016e97b39bb31b8a749a4ed2daa78f53e66256351d3aada01f5bb7ffcc5d4f8494cb0116b9816e92ba614e78dff8730b7e81b22049b73a69956a55daeec3b53c4a87f34280af1451cf67e81f804346fa1f121788af98becedb896991c8349e87eace91b73019381cc8550160742e3141ea7ef3eb0a71333496489c3e43c47fb18c076d2a9950ffe6dc6138bea1f7cd8cbf3
They are different, when I decode access token I get an object which has this key "exp":1621533539.9695 which is expiration time, but not for refresh token.
Any idea?
export const API = axios.create({
baseURL: BASE_URL,
withCredentials: false,
headers: {
Accept: "*/*",
"Access-Control-Allow-Origin": "*",
},
});
// CHECK IF TOKEN IS EXPIRED (DECODE TOKEN)
function isTokenExpired(token) {
const base64Url = token.split(".")[1];
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
const jsonPayload = decodeURIComponent(
atob(base64)
.split("")
.map(function (c) {
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
})
.join("")
);
const data = JSON.parse(jsonPayload);
const expirationDate = data.exp;
const currentDate = new Date().getTime() / 1000;
return currentDate >= expirationDate;
}
API.interceptors.request.use(
async (config) => {
let token = localStorage.getItem("accessToken");
let refreshToken = localStorage.getItem("refreshToken");
//if refresh token is expired we logout and return early
// if (refreshToken) {
// const isExpired = isTokenExpired(refreshToken);
// if (isExpired) {
// localStorage.setItem("accessToken", "");
// localStorage.setItem("refreshToken", "");
// router.push({ path: "/login" });
// }
// return config;
// }
// if token is expired we refresh the token
if (token) {
const isExpired = isTokenExpired(token);
if (isExpired) {
const data = await API.post("/token", {
grant_type: "refresh_token",
refresh_token: refreshToken,
});
if (data) {
token = data.access_token;
localStorage.setItem("accessToken", data.data.access_token);
localStorage.setItem("refreshToken", data.data.refresh_token);
}
}
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
//CHECK AFTER REQUEST IF RESPONSE IS 401
API.interceptors.response.use(
(response) => response,
async (error) => {
let refreshToken = localStorage.getItem("refreshToken");
if (error.response?.status && error.response?.status === 401) {
const data = await API.post("/token", {
grant_type: "refresh_token",
refresh_token: refreshToken,
});
if (data) {
localStorage.setItem("accessToken", data.data.access_token);
localStorage.setItem("refreshToken", data.data.refresh_token);
error.config.headers.Authorization = "Bearer " + data.access_token;
return API.request(error.config);
}
}
throw error.response;
}
);
The refresh token does't come with and expiration date.
You need to try getting a new token with it. If it is invalid, your API will tell you, then you need to send your user back to login page.
I think you know the refresh token flow, but I'll leave it here anyway:
Check if you have a token and a Refresh Token in your localStorage.
Once you have them, check if the token is expired.
if it is expired, you need to send the refresh token to your api.
The API will give you another token and refreshToken, store them and continue making requests.
If the API reject the refreshToken, send your user to login page.
I have a complete example of refreshToken and reattempt request if you need.
Everytime your token expires you need to do the refresh token process.

Different headers used in Axios patch

I spent an hour looking in the Chrome console and I cannot see where this bug comes from.
I am finishing an update of OAuth implementation in my Vue app.
The story begins when socialLink.js finds out that a new user must be created. Vue component Vue-authentication depends on the presence of access_token in a response so I return some dummy text:
return api.sendResponse(res, { email, name, socialId, access_token: 'abcd' });
The library stores this value in localStorage:
After a redirect, the SignUp.vue is rendered and I complete the form. The first communication with the server is a Vuex call to create a new user:
response = await this.$store.dispatch('CREATE_USER_PROFILE', payload);
Which returns a real short lived JWT token:
const token = auth.createToken(userId, nickname, new Date(), null, false, '1m');
return api.sendCreated(res, api.createResponse(token));
Which I store in the Vue page afterwards:
const { data } = response;
const token = data.data;
if (token === undefined) {
this.error = this.$t('sign-up.something-went-wrong');
return false;
}
I checked that the token contains what the server returned:
Request URL: https://beta.mezinamiridici.cz/api/v1/users
Request Method: POST
Status Code: 201 Created
{"success":true,"data":"eyJhbGciOiJIUzI1NiIs...Tl8JFw2HZ3VMXJk"}
Then I call another Vuex method and pass the current JWT token:
await this.$store.dispatch('UPDATE_USER_PROFILE', {
I checked in the Vuex devtools that there really is the correct JWT token. I then pass it further to api.js.
Here I create an Axios configuration holding an Authorization header:
function getAuthHeader(context, jwt = undefined, upload) {
const config = { headers: { } };
if (jwt || (context && context.rootState.users.userToken)) {
config.headers.Authorization = `bearer ${jwt || context.rootState.users.userToken}`;
}
Again, I checked that the correct JWT token is used there.
Finally, I pass all data to Axios:
function patch(endpoint, url, body, context, jwt) {
const headers = getAuthHeader(context, jwt);
console.log(headers);
if (endpoint === 'BFF') {
return axios.patch(`${VUE_APP_BFF_ENDPOINT}${url}`, body, headers);
} else {
return axios.patch(`${VUE_APP_API_ENDPOINT}${url}`, body, headers);
}
}
Which I log and can confirm the correct JWT is still there:
bearer eyJhbGciOiJIUzI1N....8JFw2HZ3VMXJk
There is nothing that could change the header now to abcd, but, the 'Network' tab shows it:
And the server fails with a parse error.
Has anybody got an idea why Axios uses the Authorization header with a different value than I pass it?
Ok, mystery solved. vue-authenticate is the reason, because, it creates Axios interceptors and handles the Authorization header itself.
vue-authenticate.common.js:
var defaultOptions = {
bindRequestInterceptor: function ($auth) {
var tokenHeader = $auth.options.tokenHeader;
$auth.$http.interceptors.request.use(function (config) {
if ($auth.isAuthenticated()) {
config.headers[tokenHeader] = [
$auth.options.tokenType, $auth.getToken()
].join(' ');
} else {
delete config.headers[tokenHeader];
}
return config
});
},
My code is more complex and it supports internal accounts with email/password so this code is breaking mine. The interceptor must be present and be a function, so the solution was:
Vue.use(VueAuthenticate, {
tokenName: 'jwt',
baseUrl: process.env.VUE_APP_API_ENDPOINT,
storageType: 'localStorage',
bindRequestInterceptor() {},
bindResponseInterceptor() {},
providers: {
facebook: {
clientId: process.env.VUE_APP_FACEBOOK_CLIENT_ID,
redirectUri: process.env.VUE_APP_FACEBOOK_REDIRECT_URI,
},

JWT Token and how to use them after login?

I like to understand the JWT handling of token.
I have created a login page to check if user exist in DB? If yes, I used jwt sign a token and return jwt token.
jwt.sign({userdata}, secretKey, (err, token) => {
res.json({
token
After I get the token I understand I have store it in local storage.
localStorage.setItem("token", token);
After this I am lost! How can I redirect the login to a protected URL once the token is stored?
Then my next question is how can I make use of the local stored token in the protected route?
For example login.html will invoke a login function call and return the token then I want to go to /admin/admin.html. In /admin/admin.html, i have protected routes that need to use the token. How can I use it ? How can I know the user is the same user using the protected route since? I know the localstored token has the user information. Does that mean every protected route I have to post a user information and compare to local token?
Some examples of the code will be useful. Thanks
You can do something like that
login() {
try{
const tk = response.token; // from api response
if (tk) {
const expiresInDuration = response.expiresIn;
setAuthTimer(expiresInDuration); // setTimer to not send rest call everytime if user is visiting many times
const now = new Date();
const expirationDate = new Date(
now.getTime() + expiresInDuration * 1000
);
this.saveAuthData(this.token, expirationDate, role);
navigate(['/home']); // function which should redirect to your desired url
}
}, (err) => {
console.log(err)
});
}
// function to auto logout after specified time
setAuthTimer(duration: number) {
this.tokenTimer = setTimeout(() => {
this.logout();
}, duration * 1000);
}
saveAuthData(token, expirationDate, role) {
localStorage.setItem('token', token);
localStorage.setItem('expiration', expirationDate.toISOString());
}
// after delete and log out
clearAuthData() {
localStorage.removeItem('token');
localStorage.removeItem('expiration');
}
// function to login user if its data is already present in the localStorage
autoAuthUser() {
authInformation = getAuthData();
if (authInformation) {
const now = new Date();
const expiresIn = authInformation.expirationDate.getTime() - now.getTime();
if (expiresIn > 0) {
this.token = authInformation.token;
this.isAuthenticated = true;
this.setAuthTimer(expiresIn / 1000);
}
}
}
For your question regarding same user is accessing the protected route as local storage is storing token specific to user that should take care of the task
You have to use a library that verifies your stored JWT token. You can use https://www.npmjs.com/package/jsonwebtoken . This library includes a method that verifies your JWT jwt.verify(token, secretOrPublicKey, [options, callback]). To be able to verify a token, you must provide the secret key that is used to sign your tokens. If the token is verified successfully, you can redirect the user to its designated page. As long as the token is stored and not expired, the user is remembered in the browser.
This is an approach for JS apps, however, if you're using PHP/Laravel, the token is stored in a HTTP cookie and I recommend using jwt-auth library, it will handle the JWT processes for you.

Categories

Resources