Browse Source

Merge pull request #2338 from nocodb/fix/insufficient-session-expiration

fix: insufficient session expiration
pull/2345/head
Pranav C 2 years ago committed by GitHub
parent
commit
c9b5111b25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      packages/nc-gui/pages/user/settings/index.vue
  2. 2
      packages/nc-gui/plugins/axiosInterceptor.js
  3. 12
      packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts
  4. 43
      packages/nocodb/src/lib/meta/api/userApi/userApis.ts
  5. 3
      packages/nocodb/src/lib/meta/helpers/ncMetaAclMw.ts
  6. 6
      packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts
  7. 37
      packages/nocodb/src/lib/migrations/v2/nc_017_add_user_token_version_column.ts
  8. 7
      packages/nocodb/src/lib/models/User.ts
  9. 6
      packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrl.ts

4
packages/nc-gui/pages/user/settings/index.vue

@ -227,8 +227,10 @@ export default {
newPassword: this.passwordDetails.newPassword 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() this.$refs.formType[0].reset()
await this.$store.dispatch('users/ActSignOut')
this.$router.push('/user/authentication/signin')
} catch (e) { } catch (e) {
this.$toast this.$toast
.error(await this._extractSdkResponseErrorMsg(e)) .error(await this._extractSdkResponseErrorMsg(e))

2
packages/nc-gui/plugins/axiosInterceptor.js

@ -77,7 +77,7 @@ export default ({ store, $axios, redirect, $toast, route, app }) => {
redirect('/') redirect('/')
} else { } else {
$toast.clear() $toast.clear()
$toast.info('Token expired please login to continue', { $toast.info('Token Expired. Please login again.', {
position: 'bottom-center' position: 'bottom-center'
}).goAway(5000) }).goAway(5000)
redirect('/user/authentication/signin') redirect('/user/authentication/signin')

12
packages/nocodb/src/lib/meta/api/userApi/initStrategies.ts

@ -53,7 +53,8 @@ export function initStrategies(router): void {
firstname, firstname,
lastname, lastname,
isAuthorized, isAuthorized,
isPublicBase isPublicBase,
token_version
}, },
done done
) { ) {
@ -72,7 +73,8 @@ export function initStrategies(router): void {
provider, provider,
firstname, firstname,
lastname, lastname,
roles roles,
token_version
}); });
}); });
@ -100,11 +102,17 @@ export function initStrategies(router): void {
); );
if (cachedVal) { if (cachedVal) {
if (cachedVal.token_version !== jwtPayload.token_version) {
return done(new Error('Token Expired. Please login again.'));
}
return done(null, cachedVal); return done(null, cachedVal);
} }
User.getByEmail(jwtPayload?.email) User.getByEmail(jwtPayload?.email)
.then(async user => { .then(async user => {
if (user.token_version !== jwtPayload.token_version) {
return done(new Error('Token Expired. Please login again.'));
}
if (req.ncProjectId) { if (req.ncProjectId) {
// this.xcMeta // this.xcMeta
// .metaGet(req.ncProjectId, null, 'nc_projects_users', { // .metaGet(req.ncProjectId, null, 'nc_projects_users', {

43
packages/nocodb/src/lib/meta/api/userApi/userApis.ts

@ -71,7 +71,8 @@ export async function signup(req: Request, res: Response<TableType>) {
password, password,
email_verification_token, email_verification_token,
invite_token: null, invite_token: null,
invite_token_expires: null invite_token_expires: null,
email: user.email
}); });
} else { } else {
NcError.badRequest('User already exist'); NcError.badRequest('User already exist');
@ -95,6 +96,8 @@ export async function signup(req: Request, res: Response<TableType>) {
} }
} }
const token_version = randomTokenString();
await User.insert({ await User.insert({
firstname, firstname,
lastname, lastname,
@ -102,7 +105,8 @@ export async function signup(req: Request, res: Response<TableType>) {
salt, salt,
password, password,
email_verification_token, email_verification_token,
roles roles,
token_version
}); });
} }
user = await User.getByEmail(email); user = await User.getByEmail(email);
@ -126,7 +130,8 @@ export async function signup(req: Request, res: Response<TableType>) {
await promisify((req as any).login.bind(req))(user); await promisify((req as any).login.bind(req))(user);
const refreshToken = randomTokenString(); const refreshToken = randomTokenString();
await User.update(user.id, { await User.update(user.id, {
refresh_token: refreshToken refresh_token: refreshToken,
email: user.email
}); });
setTokenCookie(res, refreshToken); setTokenCookie(res, refreshToken);
@ -148,7 +153,8 @@ export async function signup(req: Request, res: Response<TableType>) {
firstname: user.firstname, firstname: user.firstname,
lastname: user.lastname, lastname: user.lastname,
id: user.id, id: user.id,
roles: user.roles roles: user.roles,
token_version: user.token_version
}, },
Noco.getConfig().auth.jwt.secret, Noco.getConfig().auth.jwt.secret,
Noco.getConfig().auth.jwt.options Noco.getConfig().auth.jwt.options
@ -178,8 +184,15 @@ async function successfulSignIn({
await promisify((req as any).login.bind(req))(user); await promisify((req as any).login.bind(req))(user);
const refreshToken = randomTokenString(); const refreshToken = randomTokenString();
let token_version = user.token_version;
if (!token_version) {
token_version = randomTokenString();
}
await User.update(user.id, { await User.update(user.id, {
refresh_token: refreshToken refresh_token: refreshToken,
email: user.email,
token_version
}); });
setTokenCookie(res, refreshToken); setTokenCookie(res, refreshToken);
@ -198,7 +211,8 @@ async function successfulSignIn({
firstname: user.firstname, firstname: user.firstname,
lastname: user.lastname, lastname: user.lastname,
id: user.id, id: user.id,
roles: user.roles roles: user.roles,
token_version
}, },
Noco.getConfig().auth.jwt.secret, Noco.getConfig().auth.jwt.secret,
@ -249,6 +263,7 @@ async function googleSignin(req, res, next) {
function randomTokenString(): string { function randomTokenString(): string {
return crypto.randomBytes(40).toString('hex'); return crypto.randomBytes(40).toString('hex');
} }
function setTokenCookie(res, token): void { function setTokenCookie(res, token): void {
// create http only cookie with refresh token that expires in 7 days // create http only cookie with refresh token that expires in 7 days
const cookieOptions = { const cookieOptions = {
@ -285,7 +300,8 @@ async function passwordChange(req: Request<any, any>, res): Promise<any> {
await User.update(user.id, { await User.update(user.id, {
salt, salt,
password, password,
email: user.email email: user.email,
token_version: null
}); });
Audit.insert({ Audit.insert({
@ -311,8 +327,10 @@ async function passwordForgot(req: Request<any, any>, res): Promise<any> {
if (user) { if (user) {
const token = uuidv4(); const token = uuidv4();
await User.update(user.id, { await User.update(user.id, {
email: user.email,
reset_password_token: token, 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 { try {
const template = (await import('./ui/emailTemplates/forgotPassword')) const template = (await import('./ui/emailTemplates/forgotPassword'))
@ -363,6 +381,9 @@ async function tokenValidate(req, res): Promise<any> {
if (user.reset_password_expires < new Date()) { if (user.reset_password_expires < new Date()) {
NcError.badRequest('Password reset url expired'); NcError.badRequest('Password reset url expired');
} }
if (!user.token_version) {
NcError.badRequest('Token Expired. Please login again.');
}
res.json(true); res.json(true);
} }
@ -389,8 +410,10 @@ async function passwordReset(req, res): Promise<any> {
await User.update(user.id, { await User.update(user.id, {
salt, salt,
password, password,
email: user.email,
reset_password_expires: null, reset_password_expires: null,
reset_password_token: '' reset_password_token: '',
token_version: null
}); });
Audit.insert({ Audit.insert({
@ -416,6 +439,7 @@ async function emailVerification(req, res): Promise<any> {
} }
await User.update(user.id, { await User.update(user.id, {
email: user.email,
email_verification_token: '', email_verification_token: '',
email_verified: true email_verified: true
}); });
@ -446,6 +470,7 @@ async function refreshToken(req, res): Promise<any> {
const refreshToken = randomTokenString(); const refreshToken = randomTokenString();
await User.update(user.id, { await User.update(user.id, {
email: user.email,
refresh_token: refreshToken refresh_token: refreshToken
}); });

3
packages/nocodb/src/lib/meta/helpers/ncMetaAclMw.ts

@ -2,10 +2,11 @@ import projectAcl from '../../utils/projectAcl';
import { NextFunction, Request, Response } from 'express'; import { NextFunction, Request, Response } from 'express';
import catchError, { NcError } from './catchError'; import catchError, { NcError } from './catchError';
import extractProjectIdAndAuthenticate from './extractProjectIdAndAuthenticate'; import extractProjectIdAndAuthenticate from './extractProjectIdAndAuthenticate';
export default function(handlerFn, permissionName) { export default function(handlerFn, permissionName) {
return [ return [
extractProjectIdAndAuthenticate, extractProjectIdAndAuthenticate,
catchError(function authMiddleware(req, _res, next) { catchError(async function authMiddleware(req, _res, next) {
const roles = req?.session?.passport?.user?.roles; const roles = req?.session?.passport?.user?.roles;
if ( if (
!( !(

6
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_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_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_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 // Create a custom migration source class
export default class XcMigrationSourcev2 { export default class XcMigrationSourcev2 {
@ -18,7 +19,8 @@ export default class XcMigrationSourcev2 {
'nc_013_sync_source', 'nc_013_sync_source',
'nc_014_alter_column_data_types', 'nc_014_alter_column_data_types',
'nc_015_add_meta_col_in_column_table', '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; return nc_015_add_meta_col_in_column_table;
case 'nc_016_alter_hooklog_payload_types': case 'nc_016_alter_hooklog_payload_types':
return 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;
} }
} }
} }

37
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 <wingkwong.code@gmail.com>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/

7
packages/nocodb/src/lib/models/User.ts

@ -22,6 +22,7 @@ export default class User implements UserType {
email_verification_token?: string; email_verification_token?: string;
email_verified: boolean; email_verified: boolean;
roles?: string; roles?: string;
token_version?: string;
constructor(data: User) { constructor(data: User) {
Object.assign(this, data); Object.assign(this, data);
@ -43,7 +44,8 @@ export default class User implements UserType {
'reset_password_token', 'reset_password_token',
'email_verification_token', 'email_verification_token',
'email_verified', 'email_verified',
'roles' 'roles',
'token_version'
]); ]);
const { id } = await ncMeta.metaInsert2( const { id } = await ncMeta.metaInsert2(
null, null,
@ -71,7 +73,8 @@ export default class User implements UserType {
'reset_password_token', 'reset_password_token',
'email_verification_token', 'email_verification_token',
'email_verified', 'email_verified',
'roles' 'roles',
'token_version'
]); ]);
// get existing cache // get existing cache
const keys = [ const keys = [

6
packages/nocodb/src/lib/v1-legacy/rest/RestAuthCtrl.ts

@ -42,7 +42,8 @@ passport.serializeUser(function(
firstname, firstname,
lastname, lastname,
isAuthorized, isAuthorized,
isPublicBase isPublicBase,
token_version
}, },
done done
) { ) {
@ -61,7 +62,8 @@ passport.serializeUser(function(
provider, provider,
firstname, firstname,
lastname, lastname,
roles roles,
token_version
}); });
}); });

Loading…
Cancel
Save