From 973a7e67ba84d82ef5db762d16a3c129e74978a9 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Wed, 12 Apr 2023 12:32:39 +0530 Subject: [PATCH] feat: authguard for password change api Signed-off-by: Pranav C --- packages/nocodb-nest/src/app.module.ts | 28 +++++--- .../src/guards/global/global.guard.spec.ts | 7 ++ .../src/guards/global/global.guard.ts | 36 ++++++++++ .../src/modules/global/global.module.ts | 33 +++++++++- .../src/modules/users/users.controller.ts | 5 +- .../src/modules/users/users.module.ts | 6 +- .../src/modules/users/users.service.ts | 2 - .../src/modules/views/views.controller.ts | 4 +- .../authtoken.strategy/authtoken.strategy.ts | 10 +-- .../src/strategies/jwt.strategy.ts | 65 ++++++++++--------- 10 files changed, 140 insertions(+), 56 deletions(-) create mode 100644 packages/nocodb-nest/src/guards/global/global.guard.spec.ts create mode 100644 packages/nocodb-nest/src/guards/global/global.guard.ts diff --git a/packages/nocodb-nest/src/app.module.ts b/packages/nocodb-nest/src/app.module.ts index 540c4c97b7..00bc3d2170 100644 --- a/packages/nocodb-nest/src/app.module.ts +++ b/packages/nocodb-nest/src/app.module.ts @@ -1,12 +1,13 @@ import { Module, RequestMethod } from '@nestjs/common'; -import { APP_FILTER } from '@nestjs/core'; +import { APP_FILTER, APP_GUARD } from '@nestjs/core'; import { ExtractJwt } from 'passport-jwt'; import { Connection } from './connection/connection'; import { GlobalExceptionFilter } from './filters/global-exception/global-exception.filter'; +import { GlobalGuard } from './guards/global/global.guard'; import { GlobalMiddleware } from './middlewares/global/global.middleware'; import { AuthModule } from './modules/auth/auth.module'; import { ExtractProjectIdMiddleware } from './middlewares/extract-project-id/extract-project-id.middleware'; -import { AuthService } from './modules/auth/auth.service' +import { AuthService } from './modules/auth/auth.service'; import { UsersModule } from './modules/users/users.module'; import { MetaService } from './meta/meta.service'; import { UsersService } from './modules/users/users.service'; @@ -51,23 +52,24 @@ import { CachesModule } from './modules/caches/caches.module'; import { TestModule } from './modules/test/test.module'; import { PluginsModule } from './modules/plugins/plugins.module'; import { GlobalModule } from './modules/global/global.module'; -import { LocalStrategy } from './strategies/local.strategy' -import NcConfigFactory from './utils/NcConfigFactory' +import { LocalStrategy } from './strategies/local.strategy'; +import NcConfigFactory from './utils/NcConfigFactory'; import NcUpgrader from './version-upgrader/NcUpgrader'; +import { ClientService } from './services/client/client.service'; +import { AuthTokenStrategy } from './strategies/authtoken.strategy/authtoken.strategy'; +import { BaseViewStrategy } from './strategies/base-view.strategy/base-view.strategy'; +import { GoogleStrategy } from './strategies/google.strategy/google.strategy'; import type { MiddlewareConsumer, OnApplicationBootstrap, Provider, } from '@nestjs/common'; -import { ClientService } from './services/client/client.service'; -import { AuthTokenStrategy } from './strategies/authtoken.strategy/authtoken.strategy'; -import { BaseViewStrategy } from './strategies/base-view.strategy/base-view.strategy'; -import { GoogleStrategy } from './strategies/google.strategy/google.strategy'; +/* export const JwtStrategyProvider: Provider = { provide: JwtStrategy, useFactory: async (usersService: UsersService) => { - const config = await NcConfigFactory.make() + const config = await NcConfigFactory.make(); const options = { // ignoreExpiration: false, @@ -82,6 +84,7 @@ export const JwtStrategyProvider: Provider = { }, inject: [UsersService], }; +*/ @Module({ imports: [ @@ -129,18 +132,23 @@ export const JwtStrategyProvider: Provider = { ], controllers: [], providers: [ + // { + // provide: APP_GUARD, + // useClass: GlobalGuard, + // }, AuthService, { provide: APP_FILTER, useClass: GlobalExceptionFilter, }, - JwtStrategyProvider, + // JwtStrategyProvider, LocalStrategy, ExtractProjectIdMiddleware, ClientService, AuthTokenStrategy, BaseViewStrategy, GoogleStrategy, + // GlobalGuard, ], }) export class AppModule implements OnApplicationBootstrap { diff --git a/packages/nocodb-nest/src/guards/global/global.guard.spec.ts b/packages/nocodb-nest/src/guards/global/global.guard.spec.ts new file mode 100644 index 0000000000..a86ddbe7e4 --- /dev/null +++ b/packages/nocodb-nest/src/guards/global/global.guard.spec.ts @@ -0,0 +1,7 @@ +import { GlobalGuard } from './global.guard'; + +describe('GlobalGuard', () => { + it('should be defined', () => { + expect(new GlobalGuard()).toBeDefined(); + }); +}); diff --git a/packages/nocodb-nest/src/guards/global/global.guard.ts b/packages/nocodb-nest/src/guards/global/global.guard.ts new file mode 100644 index 0000000000..ecddca09d3 --- /dev/null +++ b/packages/nocodb-nest/src/guards/global/global.guard.ts @@ -0,0 +1,36 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { JwtStrategy } from '../../strategies/jwt.strategy'; +import type { ExecutionContext } from '@nestjs/common'; + +@Injectable() +export class GlobalGuard extends AuthGuard(['jwt']) { + constructor(private jwtStrategy: JwtStrategy) { + super(); + } + + async canActivate(context: ExecutionContext) { + let result; + try { + result = (await super.canActivate(context)) as boolean; + } catch (e) { + console.log(e); + } + if (!result) { + // If JWT authentication fails, use the fallback strategy to set a default user + const req = context.switchToHttp().getRequest(); + const user = await this.fallbackAuthenticate(req); + req.user = user; + return true; + } + return true; + } + + private async fallbackAuthenticate(req: any): Promise { + return this.jwtStrategy.validate(req, { + roles: { + guest: true, + }, + }); + } +} diff --git a/packages/nocodb-nest/src/modules/global/global.module.ts b/packages/nocodb-nest/src/modules/global/global.module.ts index 4b74816e9c..16b4696db7 100644 --- a/packages/nocodb-nest/src/modules/global/global.module.ts +++ b/packages/nocodb-nest/src/modules/global/global.module.ts @@ -1,8 +1,33 @@ -import { Global, Module } from '@nestjs/common' +import { Global, Module, Provider } from '@nestjs/common' import { JwtModule, JwtService } from '@nestjs/jwt' +import { ExtractJwt } from 'passport-jwt' import { Connection } from '../../connection/connection' +import { GlobalGuard } from '../../guards/global/global.guard' import { MetaService } from '../../meta/meta.service' +import { JwtStrategy } from '../../strategies/jwt.strategy' +import NcConfigFactory from '../../utils/NcConfigFactory' import { jwtConstants } from '../auth/constants' +import { UsersModule } from '../users/users.module' +import { UsersService } from '../users/users.service' + +export const JwtStrategyProvider: Provider = { + provide: JwtStrategy, + useFactory: async (usersService: UsersService) => { + const config = await NcConfigFactory.make(); + + const options = { + // ignoreExpiration: false, + jwtFromRequest: ExtractJwt.fromHeader('xc-auth'), + // expiresIn: '10h', + passReqToCallback: true, + secretOrKey: config.auth.jwt.secret, + ...config.auth.jwt.options, + }; + + return new JwtStrategy(options, usersService); + }, + inject: [UsersService], +}; @Global() @Module({ @@ -12,11 +37,17 @@ import { jwtConstants } from '../auth/constants' providers: [ Connection, MetaService, + UsersService, + JwtStrategyProvider, + GlobalGuard ], exports: [ Connection, MetaService, // JwtService, + JwtStrategyProvider, + UsersService, + GlobalGuard ], }) export class GlobalModule { diff --git a/packages/nocodb-nest/src/modules/users/users.controller.ts b/packages/nocodb-nest/src/modules/users/users.controller.ts index 99f1c96f8f..bf28d2f00a 100644 --- a/packages/nocodb-nest/src/modules/users/users.controller.ts +++ b/packages/nocodb-nest/src/modules/users/users.controller.ts @@ -12,6 +12,7 @@ import { } from '@nestjs/common' import * as ejs from 'ejs'; import { AuthGuard } from '@nestjs/passport'; +import { GlobalGuard } from '../../guards/global/global.guard' import { NcError } from '../../helpers/catchError'; import { Acl, @@ -142,7 +143,7 @@ export class UsersController { } @Get(['/auth/user/me', '/api/v1/db/auth/user/me', '/api/v1/auth/user/me']) - @UseGuards(ExtractProjectIdMiddleware, AuthGuard('jwt')) + @UseGuards(ExtractProjectIdMiddleware, GlobalGuard) async me(@Request() req) { const user = { ...req.user, @@ -156,7 +157,9 @@ export class UsersController { '/api/v1/db/auth/password/change', '/api/v1/auth/password/change', ]) + @UseGuards(GlobalGuard) @Acl('passwordChange') + @HttpCode(200) async passwordChange(@Request() req: any, @Body() body: any): Promise { if (!(req as any).isAuthenticated()) { NcError.forbidden('Not allowed'); diff --git a/packages/nocodb-nest/src/modules/users/users.module.ts b/packages/nocodb-nest/src/modules/users/users.module.ts index e830f1fa79..2ea075cde3 100644 --- a/packages/nocodb-nest/src/modules/users/users.module.ts +++ b/packages/nocodb-nest/src/modules/users/users.module.ts @@ -7,11 +7,7 @@ import { UsersController } from './users.controller'; @Module({ imports: [ - GlobalModule, - JwtModule.register({ - secret: jwtConstants.secret, - signOptions: { expiresIn: '10h' }, - }), + GlobalModule ], controllers: [UsersController], providers: [UsersService], diff --git a/packages/nocodb-nest/src/modules/users/users.service.ts b/packages/nocodb-nest/src/modules/users/users.service.ts index 05d5934593..43e4f641c4 100644 --- a/packages/nocodb-nest/src/modules/users/users.service.ts +++ b/packages/nocodb-nest/src/modules/users/users.service.ts @@ -1,6 +1,5 @@ import { promisify } from 'util'; import { Injectable } from '@nestjs/common'; -import { JwtService } from '@nestjs/jwt'; import { OrgUserRoles, validatePassword } from 'nocodb-sdk'; import { v4 as uuidv4 } from 'uuid'; import { isEmail } from 'validator'; @@ -28,7 +27,6 @@ import type { export class UsersService { constructor( private metaService: MetaService, - private jwtService: JwtService, ) {} async findOne(email: string) { diff --git a/packages/nocodb-nest/src/modules/views/views.controller.ts b/packages/nocodb-nest/src/modules/views/views.controller.ts index 19bc0cbb2f..9e2efa49b8 100644 --- a/packages/nocodb-nest/src/modules/views/views.controller.ts +++ b/packages/nocodb-nest/src/modules/views/views.controller.ts @@ -10,17 +10,17 @@ import { Request, UseGuards, } from '@nestjs/common'; -import { AuthGuard } from '@nestjs/passport'; import { ViewUpdateReqType } from 'nocodb-sdk'; import { PagedResponseImpl } from '../../helpers/PagedResponse'; import { ExtractProjectIdMiddleware, UseAclMiddleware, } from '../../middlewares/extract-project-id/extract-project-id.middleware'; +import { GlobalGuard } from '../../guards/global/global.guard'; import { ViewsService } from './views.service'; @Controller() -@UseGuards(ExtractProjectIdMiddleware, AuthGuard('jwt')) +@UseGuards(ExtractProjectIdMiddleware, GlobalGuard) export class ViewsController { constructor(private readonly viewsService: ViewsService) {} diff --git a/packages/nocodb-nest/src/strategies/authtoken.strategy/authtoken.strategy.ts b/packages/nocodb-nest/src/strategies/authtoken.strategy/authtoken.strategy.ts index 32282162f3..4f8c382b89 100644 --- a/packages/nocodb-nest/src/strategies/authtoken.strategy/authtoken.strategy.ts +++ b/packages/nocodb-nest/src/strategies/authtoken.strategy/authtoken.strategy.ts @@ -7,10 +7,12 @@ import type { Request } from 'express'; @Injectable() export class AuthTokenStrategy extends PassportStrategy(Strategy, 'authtoken') { constructor() { - super({ - headerFields: ['xc-token'], - passReqToCallback: true, - }); + super( + { + headerFields: ['xc-token'], + passReqToCallback: true, + }, + ); } // eslint-disable-next-line @typescript-eslint/ban-types diff --git a/packages/nocodb-nest/src/strategies/jwt.strategy.ts b/packages/nocodb-nest/src/strategies/jwt.strategy.ts index 5645012dc3..350bde85f2 100644 --- a/packages/nocodb-nest/src/strategies/jwt.strategy.ts +++ b/packages/nocodb-nest/src/strategies/jwt.strategy.ts @@ -1,16 +1,16 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common'; -import { PassportStrategy } from '@nestjs/passport'; -import { ExtractJwt, Strategy } from 'passport-jwt'; -import { OrgUserRoles } from 'nocodb-sdk'; -import NocoCache from '../cache/NocoCache'; -import { ProjectUser, User } from '../models'; -import { genJwt } from '../modules/users/helpers'; -import Noco from '../Noco'; -import extractRolesObj from '../utils/extractRolesObj'; -import { CacheGetType, CacheScope } from '../utils/globals'; -import { jwtConstants } from '../modules/auth/constants'; -import { UsersService } from '../modules/users/users.service'; -import NcConfigFactory from '../utils/NcConfigFactory'; +import { Injectable, UnauthorizedException } from '@nestjs/common' +import { PassportStrategy } from '@nestjs/passport' +import { ExtractJwt, Strategy } from 'passport-jwt' +import { OrgUserRoles } from 'nocodb-sdk' +import NocoCache from '../cache/NocoCache' +import { ProjectUser, User } from '../models' +import { genJwt } from '../modules/users/helpers' +import Noco from '../Noco' +import extractRolesObj from '../utils/extractRolesObj' +import { CacheGetType, CacheScope } from '../utils/globals' +import { jwtConstants } from '../modules/auth/constants' +import { UsersService } from '../modules/users/users.service' +import NcConfigFactory from '../utils/NcConfigFactory' @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { @@ -18,10 +18,13 @@ export class JwtStrategy extends PassportStrategy(Strategy) { super({ expiresIn: '10h', ...options, - }); + }) } async validate(req: any, jwtPayload: any) { + + if (!jwtPayload?.email) return jwtPayload + // todo: improve this if ( req.ncProjectId && @@ -31,19 +34,19 @@ export class JwtStrategy extends PassportStrategy(Strategy) { return { ...user, roles: extractRolesObj(`owner,creator,${OrgUserRoles.SUPER_ADMIN}`), - }; - }); + } + }) } - const keyVals = [jwtPayload?.email]; + const keyVals = [jwtPayload?.email] if (req.ncProjectId) { - keyVals.push(req.ncProjectId); + keyVals.push(req.ncProjectId) } - const key = keyVals.join('___'); + const key = keyVals.join('___') const cachedVal = await NocoCache.get( `${CacheScope.USER}:${key}`, CacheGetType.TYPE_OBJECT, - ); + ) if (cachedVal) { /*todo: tobe fixed @@ -54,12 +57,12 @@ export class JwtStrategy extends PassportStrategy(Strategy) { ) { throw new Error('Token Expired. Please login again.'); }*/ - return cachedVal; + return cachedVal } return User.getByEmail(jwtPayload?.email).then( async (user: { roles: any; id: string }) => { - user.roles = extractRolesObj(user?.roles); + user.roles = extractRolesObj(user?.roles) /* todo: tobe fixed if ( @@ -77,26 +80,26 @@ export class JwtStrategy extends PassportStrategy(Strategy) { return ProjectUser.get(req.ncProjectId, user.id).then( async (projectUser) => { - user.roles = extractRolesObj(projectUser?.roles || user.roles); + user.roles = extractRolesObj(projectUser?.roles || user.roles) user.roles = extractRolesObj( user.roles === 'owner' ? 'owner,creator' : user.roles, - ); + ) // + (user.roles ? `,${user.roles}` : ''); - await NocoCache.set(`${CacheScope.USER}:${key}`, user); - return user; + await NocoCache.set(`${CacheScope.USER}:${key}`, user) + return user }, - ); + ) } else { // const roles = projectUser?.roles ? JSON.parse(projectUser.roles) : {guest: true}; if (user) { - await NocoCache.set(`${CacheScope.USER}:${key}`, user); - return user; + await NocoCache.set(`${CacheScope.USER}:${key}`, user) + return user } else { - throw new Error('User not found'); + throw new Error('User not found') } } }, - ); + ) } }