Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for activeDuration (or an example in the doc how to do that #237

Open
shai32 opened this issue May 14, 2017 · 15 comments
Open

Comments

@shai32
Copy link

shai32 commented May 14, 2017

activeDuration allows users to lengthen their session by interacting with the site. If the session is 28 minutes old and the user sends another request, activeDuration will extend the session’s life for however long you define. In this case, 15 minutes.
In short, activeDuration prevents the app from logging a user out while they’re still using the site.

example why its important:
let say the user get a token with a long expiration date ( lets say 7 days)
when he log in, he clicked on remember me.
now, the user uses the site every day, after 7 days, it still has his original token (that is stored in is browser)
he open the website, no login need (token has still 1 minute left), and then suddenly after 1 min of use, the website throw him out.

the user does not understand what happened and why.

so we need a way to define some sort of activeDuration:

it sound like a must have for any site that want a good user experience.
am I wrong? what most site do?

@nelsonic
Copy link
Member

@shai32 thanks for opening this issue to discuss activeDuration.
The beauty of hapi-auth-jwt2 is that it allows you to implement any feature in your validateFunc function. so you can have a value for activeDuration and treat it how ever you chose in your application. 👍

@shai32
Copy link
Author

shai32 commented May 14, 2017

@nelsonic validateFunc is very limited

  1. you can't create a new token and pass it as an header to the client, you can only call callback with errr, valid and credentials:
    callback - (required) a callback function with the signature function(err, isValid, credentials) where:
    err - an internal error.
    valid - true if the JWT was valid, otherwise false.
    credentials - (optional) alternative credentials to be set instead of decoded.

  2. so if I want to return a new token to the client, I need to do it on each route handler (the logic of creating new token and putting it in the header). all my api calls should have this logic to return the new token when expired. so adding this logic to every handler is a bad practice and a lot of code duplication.
    how do you guys, solve it?

help with how to code it, will be very appreciated.

@shai32
Copy link
Author

shai32 commented May 14, 2017

I want to be able to send a new token to the user not only for expiration, also for renewing invalidate data. the decoded token itself contains a data for the user (like user role, user id)
if this data is invalidate (for example the user details is . updated). I want to be able to send the client a new token with the new refreshed data.
how do I do that in a one central place that will be applied to all api calls?

@nelsonic
Copy link
Member

@shai32 if validateFunc is limited, then use verifyFunc which gives you complete freedom to implement anything.

@shai32
Copy link
Author

shai32 commented May 14, 2017

@nelsonic verifyFunc has the same limitation
please someone else have a solution that works?

@bitcloud
Copy link
Contributor

I don't think that is part of the auth. Same as creating the token you use in the first place.

We solved this issue with a separate session management plugin that checks in a hook for every call if the token is still valid or should be extended. If it should be extended, it attaches the new token to the header/cookie and passes on to the rest.

so you keep everything nicely separated ;-)

@bitcloud
Copy link
Contributor

just checked. We implemented it on the onPreResponse hook.

@shai32
Copy link
Author

shai32 commented May 14, 2017

@bitcloud sound interesting, so validateFunc isn't needed at all, can I use onPreResponse to test the token/session ?

@bitcloud
Copy link
Contributor

As it still has pass auth-jwt2 with a valid token, which takes care of the decoding, you have access to the request.auth object like in any other route handler.

We pick the credentials from request.auth, use a refreshTime value to check if a token is due for replacement, if it is due for replacement we extend the timeout in the credentials and issue a new token.

e.g. Lifetime is 7 days. Refresh is 1 day. So if the token has a lifetime of less then a day, a new one is issued.

@shai32
Copy link
Author

shai32 commented May 14, 2017

@bitcloud thats exactly what I need, Thanks.
I will try it.

I want to use a Lifetime of 7 days. and Refresh as 6 days. in this way I am ensuring that the user has 6-7 days to use it form last visit my site.

what do you think about this numbers, are there ok?

also I want to write unit test for this, can I mock the server clock to test it?

@bitcloud
Copy link
Contributor

yeah, numbers are fine! the only thing you want to avoid is creating a token every request.
and one token a day per user seams reasonable for most cases. ;-)

Unit tests should be easy. You just need to mock request and reply and expose the handler in the module.
Mocking the clock is an issue by itself. There are different solutions out there depending on your setup. Like if you are using jasmine for testing i think its jasmin.clock().install() or something. sinonjs provides something as well. Just give Google a spin and check what fits your need.

You should even be able to integration tests with the server.inject method pretty straight forward.

@shai32
Copy link
Author

shai32 commented May 15, 2017

@bitcloud Thanks for your help, it works now, this is my code if anyone else need to implement activeDuration and creating a new token if credentials need to be updated.


exports.register = (server, options, callback) => {
  const validate = async (decoded, request, callback) => {
    try {
      const session = await db.Sessions.where({ id: decoded.sessionId }).first();
      if (_.isEmpty(session)) {
        return callback(null, false);
      }

      const tokenExpirationDate = moment(decoded.exp * 1000);
      if (session.invalidate || tokenExpirationDate.diff(moment(), 'days') < 6) {
        const user = await db.Users.where({ id: decoded.id }).first();
        const session = await db.functions.createUserSession(user.id);

        const credentials =credentialsNewCredentials(session.id, user);
       credentials.renew = true;
        return callback(null, true, credentials);
      }

      return callback(null, true);
    } catch (error) {
      throw error;
    }
  };

  server.auth.strategy('jwt', 'jwt', {
    key: process.env.AUTH_SECRET_KEY, // Never Share your secret key
    validateFunc: validate, // validate function defined above
    verifyOptions: {
      algorithms: ['HS256']
    }
  });

  server.ext('onPreResponse', function(request, reply) {
    const response = request.response;
    if (response.isBoom) {
      return reply.continue();
    }

    const credentials = reply.request.auth.credentials;
    if (credentials && credentials.renew) {
      delete credentials.renew;
      reply.request.response.headers['authorization'] = JWT.sign(credentials, 
            process.env.AUTH_SECRET_KEY, { expiresIn: `7d` });
    }

    reply.continue();
  });

  return callback();
};

exports.register.attributes = {
  name: `plugins-auth`,
  dependencies: [`hapi-auth-jwt2`]
};

@bitcloud
Copy link
Contributor

Concerning the first part: This means that you recreate a Token when you invalidate the session? I would have guessed that this would just delete the session :-)
And I currently see no other way invalidate the session from the server side. So once you have a token you'll stay logged in?

And in the second part: the renew only happens once as you delete the flag in the credentials. What should happen the second time? The user won't see the difference, or will this be checked later on in the app again?

and I think a nicer way to set headers and cookies would be

      request.response
        .header('Authorization', 'Token ' + token)
        .state('token', token, Config.cookie);

Probably I don't understand the whole idea of activeDuration. I just call it JWT refresh. Because it just refreshes your token when it is approaching end of life. And there is no difference to the original token besides the expiry.

But you are certainly on the right track :-)

@shai32
Copy link
Author

shai32 commented May 16, 2017

@bitcloud I have different propose of using invalidate.
when I want to log the user out, I am deleting the session.
but in my case, the session encoded in the token contains the user data (save me from fetching the user from the database in each request).
so when I am updating the user data, I am marking all the session related to this user as invalidate.
and my code auto replaces the token with the new data automatically.

this way I am ensuring that client always as the updated session.

regarding the second part, the renew should only happen once and its ok, the client gets a new token with a new session. this token after one day will also be replaced again.
if the user does not use my site for 7 days, the token will be expired, and the user will be logged out.

activeDuration as indeed JWT refresh, but I implemented it also to refresh the session data inside the token (only when needed), not only the expiration.

I need the user data (such as role, displayName and more) to always be up to date with the client.
is there a better way?

@shai32
Copy link
Author

shai32 commented Dec 25, 2017

here is my updated code that does it

const _ = require('lodash');
const moment = require('moment');
const db = require('db');
const { getClientData, log } = require('utils');
const { signToken } = require('services/authentication');

exports.register = (server, options, next) => {
                const validate = async (decoded, request, validationCallback) => {
		try {
			const { auth } = request;

			auth.session = await db.Sessions.where({ id: decoded.sessionId }).first();
			if (_.isEmpty(auth.session)) {
				return validationCallback(null, false);
			}

			if (auth.session.invalidate) {
				auth.renew = true;
				const { session } = auth;
				auth.session = await db.functions.createUserSession(session.userId, getClientData(request), session.id);
				log.info('Session was renewd', { oldSession: session, newSession: auth.session });
				const credentials = { sessionId: auth.session.id };
				auth.session.token = signToken(credentials);
				return validationCallback(null, true, credentials);
			}

			const tokenExpirationDate = moment(decoded.exp * 1000);
			const daysLeft = tokenExpirationDate.diff(moment(), 'days', true);
			if (daysLeft < 6) {
				auth.renew = true;
				log.info(`Session token was extended from ${daysLeft.toFixed(2)} days to default (7)`);
				auth.session.token = signToken({ sessionId: auth.session.id });
			}
			return validationCallback(null, true);
		} catch (error) {
			return validationCallback(error, false);
		}
	};

	server.auth.strategy('jwt', 'jwt', {
		key: server.app.AUTH_SECRET_KEY, // Never Share your secret key
		validateFunc: validate, // validate function defined above
		verifyOptions: {
			// ignoreExpiration: true,
			algorithms: ['HS256']
		}
	});

	server.ext('onPreResponse', async (request, reply) => {
		const { response, auth } = request;

		if (auth.renew) {
			const headers = response.isBoom ? response.output.headers : response.headers;
			headers['WWW-Authenticate'] = JSON.stringify(auth.session);
		}

		return reply.continue();
	});

	return next();
};

exports.register.attributes = {
	name: 'auth',
	dependencies: ['hapi-auth-jwt2']
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants