diff --git a/config/index.js b/config/index.js index 71fbc3f..d1e7127 100644 --- a/config/index.js +++ b/config/index.js @@ -84,6 +84,17 @@ module.exports = { */ dateFormat: 'YYYY-MM-DD HH:mm:ss', + /* + |-------------------------------------------------------------------------- + | Valid For + |-------------------------------------------------------------------------- + | + | The default time in hours the token is valid for. This can be changed for + | an individual token by calling `isValidFor()` before generating it. + | + */ + validFor: 24, + /* |-------------------------------------------------------------------------- | Validation messages diff --git a/src/Persona.js b/src/Persona.js index 08e3441..625255d 100644 --- a/src/Persona.js +++ b/src/Persona.js @@ -44,7 +44,8 @@ class Persona { model: 'App/Models/User', newAccountState: 'pending', verifiedAccountState: 'active', - dateFormat: 'YYYY-MM-DD HH:mm:ss' + dateFormat: 'YYYY-MM-DD HH:mm:ss', + validFor: 24 }) /** @@ -59,6 +60,7 @@ class Persona { this._encrypter = Encryption.getInstance({ hmac: false }) this._model = null + this._validFor = null } /** @@ -157,7 +159,7 @@ class Persona { query .where('type', type) .where('is_revoked', false) - .where('updated_at', '>=', moment().subtract(24, 'hours').format(this.config.dateFormat)) + .where('expires_at', '>=', moment().format(this.config.dateFormat)) } /** @@ -187,7 +189,16 @@ class Persona { } const token = this._encrypter.encrypt(randtoken.generate(16)) - await user.tokens().create({ type, token }) + const expiresAt = moment().add(this.getValidFor(), 'hours').format(this.config.dateFormat) + + this._validFor = null + + await user.tokens().create({ + type, + token, + expires_at: expiresAt + }) + return token } @@ -276,6 +287,36 @@ class Persona { return this.getModel().table } + /** + * Returns the duration in hours the a generated token should be valid for + * + * @method getValidFor + * + * @return {Number} + */ + getValidFor () { + if (this._validFor) { + return this._validFor + } + + return this.config.validFor + } + + /** + * Sets the duration in hours the a generated token should be valid for + * + * @method isValidFor + * + * @param {Number} payload + * + * @return {Persona} + */ + isValidFor (payload) { + this._validFor = payload + + return this + } + /** * Returns an object of registeration rules * diff --git a/test/persona.spec.js b/test/persona.spec.js index 88fc13a..bf8ba89 100644 --- a/test/persona.spec.js +++ b/test/persona.spec.js @@ -206,7 +206,8 @@ test.group('Persona', (group) => { user_id: user.id, is_revoked: false, created_at: moment().subtract(2, 'days').format('YYYY-MM-DD HH:mm:ss'), - updated_at: moment().subtract(2, 'days').format('YYYY-MM-DD HH:mm:ss') + updated_at: moment().subtract(2, 'days').format('YYYY-MM-DD HH:mm:ss'), + expires_at: moment().subtract(1, 'days').format('YYYY-MM-DD HH:mm:ss') }) assert.plan(2) @@ -228,7 +229,8 @@ test.group('Persona', (group) => { user_id: user.id, is_revoked: false, created_at: moment().format('YYYY-MM-DD HH:mm:ss'), - updated_at: moment().format('YYYY-MM-DD HH:mm:ss') + updated_at: moment().format('YYYY-MM-DD HH:mm:ss'), + expires_at: moment().add(2, 'days').format('YYYY-MM-DD HH:mm:ss') }) assert.plan(2) @@ -250,7 +252,8 @@ test.group('Persona', (group) => { is_revoked: false, user_id: user.id, created_at: moment().format('YYYY-MM-DD HH:mm:ss'), - updated_at: moment().format('YYYY-MM-DD HH:mm:ss') + updated_at: moment().format('YYYY-MM-DD HH:mm:ss'), + expires_at: moment().add(2, 'days').format('YYYY-MM-DD HH:mm:ss') }) await this.persona.verifyEmail('hello') @@ -271,7 +274,8 @@ test.group('Persona', (group) => { is_revoked: false, user_id: user.id, created_at: moment().format('YYYY-MM-DD HH:mm:ss'), - updated_at: moment().format('YYYY-MM-DD HH:mm:ss') + updated_at: moment().format('YYYY-MM-DD HH:mm:ss'), + expires_at: moment().add(2, 'days').format('YYYY-MM-DD HH:mm:ss') }) await this.persona.verifyEmail('hello') @@ -604,7 +608,8 @@ test.group('Persona', (group) => { user_id: user.id, is_revoked: false, created_at: moment().format('YYYY-MM-DD HH:mm:ss'), - updated_at: moment().format('YYYY-MM-DD HH:mm:ss') + updated_at: moment().format('YYYY-MM-DD HH:mm:ss'), + expires_at: moment().add(2, 'days').format('YYYY-MM-DD HH:mm:ss') }) try { @@ -629,7 +634,8 @@ test.group('Persona', (group) => { user_id: user.id, is_revoked: false, created_at: moment().subtract(2, 'days').format('YYYY-MM-DD HH:mm:ss'), - updated_at: moment().subtract(2, 'days').format('YYYY-MM-DD HH:mm:ss') + updated_at: moment().subtract(2, 'days').format('YYYY-MM-DD HH:mm:ss'), + expires_at: moment().subtract(1, 'days').format('YYYY-MM-DD HH:mm:ss') }) try { @@ -655,7 +661,8 @@ test.group('Persona', (group) => { user_id: user.id, is_revoked: false, created_at: moment().format('YYYY-MM-DD HH:mm:ss'), - updated_at: moment().format('YYYY-MM-DD HH:mm:ss') + updated_at: moment().format('YYYY-MM-DD HH:mm:ss'), + expires_at: moment().add(2, 'days').format('YYYY-MM-DD HH:mm:ss') }) await this.persona.updatePasswordByToken('hello', { password: 'newsecret', password_confirmation: 'newsecret' }) @@ -749,4 +756,58 @@ test.group('Persona', (group) => { assert.equal(users.size(), 1) assert.equal(users.first().email, 'foo@bar.com') }) + + test('generated tokens are valid for 24 hours by default', async (assert) => { + const user = await getUser().create({ + username: 'virk', + email: 'foo@bar.com', + account_status: 'active', + password: 'secret' + }) + + const token = await this.persona.generateToken(user, 'email') + + const tokenEntity = await this.persona.getToken(token, 'email') + assert.equal(tokenEntity.expires_at, moment().add(1, 'days').format('YYYY-MM-DD HH:mm:ss')) + }) + + test('tokens without an expiry date are invalid', async (assert) => { + const user = await getUser().create({ email: 'foo@bar.com' }) + + await use('Database').table('tokens').insert({ + token: 'hello', + type: 'email', + user_id: user.id, + is_revoked: false, + created_at: moment().subtract(2, 'days').format('YYYY-MM-DD HH:mm:ss'), + updated_at: moment().subtract(2, 'days').format('YYYY-MM-DD HH:mm:ss'), + expires_at: null + }) + + assert.plan(2) + + try { + await this.persona.verifyEmail('hello') + } catch ({ message, name }) { + assert.equal(message, 'The token is invalid or expired') + assert.equal(name, 'InvalidTokenException') + } + }) + + test('the expiry date for a tokens can be changed for each token', async (assert) => { + const user = await getUser().create({ + username: 'virk', + email: 'foo@bar.com', + account_status: 'active', + password: 'secret' + }) + + const token = await this.persona.isValidFor(48).generateToken(user, 'email') + const tokenEntity = await this.persona.getToken(token, 'email') + assert.equal(tokenEntity.expires_at, moment().add(2, 'days').format('YYYY-MM-DD HH:mm:ss')) + + const token1 = await this.persona.generateToken(user, 'password') + const tokenEntity1 = await this.persona.getToken(token1, 'password') + assert.equal(tokenEntity1.expires_at, moment().add(1, 'days').format('YYYY-MM-DD HH:mm:ss')) + }) }) diff --git a/test/setup.js b/test/setup.js index 105076b..44267bd 100644 --- a/test/setup.js +++ b/test/setup.js @@ -76,6 +76,7 @@ module.exports = { table.string('token').notNull() table.string('type').notNull() table.boolean('is_revoked').defaultsTo(false) + table.timestamp('expires_at').nullable() table.timestamps() }) },