diff --git a/packages/nc-gui/pages/user/settings/index.vue b/packages/nc-gui/pages/user/settings/index.vue index 3949a22d107..e214494673b 100644 --- a/packages/nc-gui/pages/user/settings/index.vue +++ b/packages/nc-gui/pages/user/settings/index.vue @@ -227,8 +227,10 @@ export default { newPassword: this.passwordDetails.newPassword } ) - this.$toast.success('Password changed successfully.').goAway(3000) + this.$toast.success('Password changed successfully. Please login again.').goAway(3000) this.$refs.formType[0].reset() + await this.$store.dispatch('users/ActSignOut') + this.$router.push('/user/authentication/signin') } catch (e) { this.$toast .error(await this._extractSdkResponseErrorMsg(e)) diff --git a/packages/nc-gui/plugins/axiosInterceptor.js b/packages/nc-gui/plugins/axiosInterceptor.js index 966610a8183..5529b0704a8 100644 --- a/packages/nc-gui/plugins/axiosInterceptor.js +++ b/packages/nc-gui/plugins/axiosInterceptor.js @@ -77,7 +77,7 @@ export default ({ store, $axios, redirect, $toast, route, app }) => { redirect('/') } else { $toast.clear() - $toast.info('Token expired please login to continue', { + $toast.info('Token Expired. Please login again.', { position: 'bottom-center' }).goAway(5000) redirect('/user/authentication/signin') diff --git a/packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts b/packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts index 1ffe03e98c8..5eceff31907 100644 --- a/packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts +++ b/packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts @@ -53,7 +53,8 @@ export function initStrategies(router): void { firstname, lastname, isAuthorized, - isPublicBase + isPublicBase, + token_version }, done ) { @@ -72,7 +73,8 @@ export function initStrategies(router): void { provider, firstname, lastname, - roles + roles, + token_version }); }); @@ -100,11 +102,17 @@ export function initStrategies(router): void { ); if (cachedVal) { + if (cachedVal.token_version !== jwtPayload.token_version) { + return done(new Error('Token Expired. Please login again.')); + } return done(null, cachedVal); } User.getByEmail(jwtPayload?.email) .then(async user => { + if (user.token_version !== jwtPayload.token_version) { + return done(new Error('Token Expired. Please login again.')); + } if (req.ncProjectId) { // this.xcMeta // .metaGet(req.ncProjectId, null, 'nc_projects_users', { diff --git a/packages/nocodb/src/lib/meta/api/userApi/userApis.ts b/packages/nocodb/src/lib/meta/api/userApi/userApis.ts index 927bb1c6e30..2b8384a9c6e 100644 --- a/packages/nocodb/src/lib/meta/api/userApi/userApis.ts +++ b/packages/nocodb/src/lib/meta/api/userApi/userApis.ts @@ -71,7 +71,8 @@ export async function signup(req: Request, res: Response) { password, email_verification_token, invite_token: null, - invite_token_expires: null + invite_token_expires: null, + email: user.email }); } else { NcError.badRequest('User already exist'); @@ -95,6 +96,8 @@ export async function signup(req: Request, res: Response) { } } + const token_version = randomTokenString(); + await User.insert({ firstname, lastname, @@ -102,7 +105,8 @@ export async function signup(req: Request, res: Response) { salt, password, email_verification_token, - roles + roles, + token_version }); } user = await User.getByEmail(email); @@ -126,7 +130,8 @@ export async function signup(req: Request, res: Response) { await promisify((req as any).login.bind(req))(user); const refreshToken = randomTokenString(); await User.update(user.id, { - refresh_token: refreshToken + refresh_token: refreshToken, + email: user.email }); setTokenCookie(res, refreshToken); @@ -148,7 +153,8 @@ export async function signup(req: Request, res: Response) { firstname: user.firstname, lastname: user.lastname, id: user.id, - roles: user.roles + roles: user.roles, + token_version: user.token_version }, Noco.getConfig().auth.jwt.secret, Noco.getConfig().auth.jwt.options @@ -178,8 +184,15 @@ async function successfulSignIn({ await promisify((req as any).login.bind(req))(user); const refreshToken = randomTokenString(); + let token_version = user.token_version; + if (!token_version) { + token_version = randomTokenString(); + } + await User.update(user.id, { - refresh_token: refreshToken + refresh_token: refreshToken, + email: user.email, + token_version }); setTokenCookie(res, refreshToken); @@ -198,7 +211,8 @@ async function successfulSignIn({ firstname: user.firstname, lastname: user.lastname, id: user.id, - roles: user.roles + roles: user.roles, + token_version }, Noco.getConfig().auth.jwt.secret, @@ -249,6 +263,7 @@ async function googleSignin(req, res, next) { function randomTokenString(): string { return crypto.randomBytes(40).toString('hex'); } + function setTokenCookie(res, token): void { // create http only cookie with refresh token that expires in 7 days const cookieOptions = { @@ -285,7 +300,8 @@ async function passwordChange(req: Request, res): Promise { await User.update(user.id, { salt, password, - email: user.email + email: user.email, + token_version: null }); Audit.insert({ @@ -311,8 +327,10 @@ async function passwordForgot(req: Request, res): Promise { if (user) { const token = uuidv4(); await User.update(user.id, { + email: user.email, reset_password_token: token, - reset_password_expires: new Date(Date.now() + 60 * 60 * 1000) + reset_password_expires: new Date(Date.now() + 60 * 60 * 1000), + token_version: null }); try { const template = (await import('./ui/emailTemplates/forgotPassword')) @@ -363,6 +381,9 @@ async function tokenValidate(req, res): Promise { if (user.reset_password_expires < new Date()) { NcError.badRequest('Password reset url expired'); } + if (!user.token_version) { + NcError.badRequest('Token Expired. Please login again.'); + } res.json(true); } @@ -389,8 +410,10 @@ async function passwordReset(req, res): Promise { await User.update(user.id, { salt, password, + email: user.email, reset_password_expires: null, - reset_password_token: '' + reset_password_token: '', + token_version: null }); Audit.insert({ @@ -416,6 +439,7 @@ async function emailVerification(req, res): Promise { } await User.update(user.id, { + email: user.email, email_verification_token: '', email_verified: true }); @@ -446,6 +470,7 @@ async function refreshToken(req, res): Promise { const refreshToken = randomTokenString(); await User.update(user.id, { + email: user.email, refresh_token: refreshToken }); diff --git a/packages/nocodb/src/lib/meta/helpers/ncMetaAclMw.ts b/packages/nocodb/src/lib/meta/helpers/ncMetaAclMw.ts index 2164bb030f8..944ab25de09 100644 --- a/packages/nocodb/src/lib/meta/helpers/ncMetaAclMw.ts +++ b/packages/nocodb/src/lib/meta/helpers/ncMetaAclMw.ts @@ -2,10 +2,11 @@ import projectAcl from '../../utils/projectAcl'; import { NextFunction, Request, Response } from 'express'; import catchError, { NcError } from './catchError'; import extractProjectIdAndAuthenticate from './extractProjectIdAndAuthenticate'; + export default function(handlerFn, permissionName) { return [ extractProjectIdAndAuthenticate, - catchError(function authMiddleware(req, _res, next) { + catchError(async function authMiddleware(req, _res, next) { const roles = req?.session?.passport?.user?.roles; if ( !( diff --git a/packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts b/packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts index 7d0de4dc263..dbcd223c7bc 100644 --- a/packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts +++ b/packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts @@ -4,6 +4,7 @@ import * as nc_013_sync_source from './v2/nc_013_sync_source'; import * as nc_014_alter_column_data_types from './v2/nc_014_alter_column_data_types'; import * as nc_015_add_meta_col_in_column_table from './v2/nc_015_add_meta_col_in_column_table'; import * as nc_016_alter_hooklog_payload_types from './v2/nc_016_alter_hooklog_payload_types'; +import * as nc_017_add_user_token_version_column from './v2/nc_017_add_user_token_version_column'; // Create a custom migration source class export default class XcMigrationSourcev2 { @@ -18,7 +19,8 @@ export default class XcMigrationSourcev2 { 'nc_013_sync_source', 'nc_014_alter_column_data_types', 'nc_015_add_meta_col_in_column_table', - 'nc_016_alter_hooklog_payload_types' + 'nc_016_alter_hooklog_payload_types', + 'nc_017_add_user_token_version_column' ]); } @@ -40,6 +42,8 @@ export default class XcMigrationSourcev2 { return nc_015_add_meta_col_in_column_table; case 'nc_016_alter_hooklog_payload_types': return nc_016_alter_hooklog_payload_types; + case 'nc_017_add_user_token_version_column': + return nc_017_add_user_token_version_column; } } } diff --git a/packages/nocodb/src/lib/migrations/v2/nc_017_add_user_token_version_column.ts b/packages/nocodb/src/lib/migrations/v2/nc_017_add_user_token_version_column.ts new file mode 100644 index 00000000000..083bcb353d5 --- /dev/null +++ b/packages/nocodb/src/lib/migrations/v2/nc_017_add_user_token_version_column.ts @@ -0,0 +1,37 @@ +import Knex from 'knex'; + +const up = async (knex: Knex) => { + await knex.schema.alterTable('nc_users_v2', table => { + table.string('token_version'); + }); +}; + +const down = async knex => { + await knex.schema.alterTable('nc_users_v2', table => { + table.dropColumns('token_version'); + }); +}; + +export { up, down }; + +/** + * @copyright Copyright (c) 2021, Xgene Cloud Ltd + * + * @author Wing-Kam Wong + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ diff --git a/packages/nocodb/src/lib/models/User.ts b/packages/nocodb/src/lib/models/User.ts index 92792da92d5..3b383dc88c5 100644 --- a/packages/nocodb/src/lib/models/User.ts +++ b/packages/nocodb/src/lib/models/User.ts @@ -22,6 +22,7 @@ export default class User implements UserType { email_verification_token?: string; email_verified: boolean; roles?: string; + token_version?: string; constructor(data: User) { Object.assign(this, data); @@ -43,7 +44,8 @@ export default class User implements UserType { 'reset_password_token', 'email_verification_token', 'email_verified', - 'roles' + 'roles', + 'token_version' ]); const { id } = await ncMeta.metaInsert2( null, @@ -71,7 +73,8 @@ export default class User implements UserType { 'reset_password_token', 'email_verification_token', 'email_verified', - 'roles' + 'roles', + 'token_version' ]); // get existing cache const keys = [ diff --git a/packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrl.ts b/packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrl.ts index f7c0a2fc7e0..a7ac48db873 100644 --- a/packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrl.ts +++ b/packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrl.ts @@ -42,7 +42,8 @@ passport.serializeUser(function( firstname, lastname, isAuthorized, - isPublicBase + isPublicBase, + token_version }, done ) { @@ -61,7 +62,8 @@ passport.serializeUser(function( provider, firstname, lastname, - roles + roles, + token_version }); });