Nestjs Interceptor how to catch http 401 error and resubmit original request - nestjs

I need to write an http header interceptor to add Authorization header, if there is a 401 error, submit another request for a new token, then resubmit the original request with the new token.
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
return next.handle().pipe(
catchError(async error => {
if (error.response.status === 401) {
const originalRequest = error.config;
var authRes = await this.authenticationService.getAccessToken();
this.authenticationService.accessTokenSubject.next(authRes.access_token);
// I need to resubmit the original request with the new token from here
// but return next.handle(originalRequest) doesn't work
}
return throwError(error);
}),
);
}
But next.handle(originalRequest) doesn't work. How to resubmit the original request in the interceptor? Thank you very much in advance for your help.

I just encountered a similar problem, where I can catch the exception from exception filter but can't do so in interception layer.
So I looked up the manual and found it says:
Any exception thrown by a guard will be handled by the exceptions layer
(global exceptions filter and any exceptions filters that are applied to the current context).
So, if the exception is thrown from AuthGuard context(including the validate method in your AuthService), probably better to move the additional logic by extending the Authguard
like this:
export class CustomizedAuthGuard extends AuthGuard('strategy') {
handleRequest(err, user, info, context, status) {
if (err || !user) {
// your logic here
throw err || new UnauthorizedException();
}
return user;
}
}
or simply using customized exception filter.

It's been a while since the question but maybe it will help someone.
Ok, suppose that we need handle unauthorize exception out of route and guards, maybe service to service. So you can implement a interceptor like that and add some logic to get some data if needed, Ex: inject some Service in the interceptor.
So, throw an unauthorize exception and we are going to intercept it:
#Injectable()
export class UnauthorizedInterceptor implements NestInterceptor {
constructor(
private readonly authService: AuthService,
private readonly httpService: HttpService,
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError((err) => {
const {
response: { status, config },
} = err;
// assuming we have a request body
const jsonData = JSON.parse(config.data);
if (status === HttpStatus.UNAUTHORIZED) {
// We can use some data in payload to find user data
// here for example the user email
if (jsonData?.email) {
return
from(this.authService.getByUserEmail(jsonData.email)).pipe(
switchMap((user: User) => {
if (user) {
// Ex: we can have stored token info in user entity.
// call function to refresh access token and update user data
// with new tokens
return from(this.authService.refreshToken(user)).pipe(
switchMap((updatedUser: User) => {
// now updatedUser have the new accessToken
const { accessToken } = updatedUser;
// set the new token to config (original request)
config.headers['Authorization'] = `Bearer ${accessToken}`;
// and use the underlying Axios instance created by #nestjs/axios
// to resubmit the original request
return of(this.httpService.axiosRef(config));
}),
);
}
}),
);
} else {
return throwError(() => new HttpException(err, Number(err.code)));
}
} else {
return throwError(() => new HttpException(err, Number(err.code)));
}
}),
);
}
}

Related

Exception Interceptor should modify res

I implemented an exception interceptor to intercept my HttpExceptions and return a http response depending on which HttpException is thrown by my application.
In my case I am throwing a 403 http exception in another interceptor which used by a route and declare the exception interceptor as a global one.
My interceptor looks like:
#Injectable()
export class ExceptionInterceptor implements NestInterceptor<Response> {
constructor(
private readonly logger: LoggerService
) {
this.logger.setContext(this.constructor.name);
}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const ctx = context.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const url = getOriginalUrl(request);
return next.handle().pipe(
catchError(
exception => {
switch (exception.status) {
case HttpStatus.FORBIDDEN: {
const forbiddenResBody = getForbiddenResBody(exception.response, url);
response.status(HttpStatus.FORBIDDEN);
response.header('Content-type', 'application/problem+json');
this.logger.error(
'Unauthorized',
exception
)
return of(forbiddenResBody)
}
default: {
const invalidServerResBody = getInternalServerResBody(url);
console.log('it is default')
this.logger.error(
'Unexpected exception',
exception
)
return of(invalidServerResBody);
}
}
})
);
}
}
However, the response is not modified is there anything that I am missing ?
Any help would be really appreciated !

In nestjs, how can we change default error messages from typeORM globally?

I have this code to change the default message from typeorm when a value in a unique column already exists. It just creates a custom message when we get an error 23505.
if (error.code === '23505') {
// message = This COLUMN VALUE already exists.
const message = error.detail.replace(
/^Key \((.*)\)=\((.*)\) (.*)/,
'The $1 $2 already exists.',
);
throw new BadRequestException(message);
}
throw new InternalServerErrorException();
I will have to use it in other services, so I would like to abstract that code.
I think I could just create a helper and then I import and call it wherever I need it. But I don’t know if there is a better solution to use it globally with a filter or an interceptor, so I don’t have to even import and call it in different services.
Is this possible? how can that be done?
If it is not possible, what do you think the best solution would be?
Here all the service code:
#Injectable()
export class MerchantsService {
constructor(
#InjectRepository(Merchant)
private merchantRepository: Repository<Merchant>,
) {}
public async create(createMerchantDto: CreateMerchantDto) {
try {
const user = this.merchantRepository.create({
...createMerchantDto,
documentType: DocumentType.NIT,
isActive: false,
});
await this.merchantRepository.save(user);
const { password, ...merchantData } = createMerchantDto;
return {
...merchantData,
};
} catch (error) {
if (error.code === '23505') {
// message = This COLUMN VALUE already exists.
const message = error.detail.replace(
/^Key \((.*)\)=\((.*)\) (.*)/,
'The $1 $2 already exists.',
);
throw new BadRequestException(message);
}
throw new InternalServerErrorException();
}
}
public async findOneByEmail(email: string): Promise<Merchant | null> {
return this.merchantRepository.findOneBy({ email });
}
}
I created an exception filter for typeORM errors.
This was the result:
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpStatus,
InternalServerErrorException,
} from '#nestjs/common';
import { Response } from 'express';
import { QueryFailedError, TypeORMError } from 'typeorm';
type ExceptionResponse = {
statusCode: number;
message: string;
};
#Catch(TypeORMError, QueryFailedError)
export class TypeORMExceptionFilter implements ExceptionFilter {
private defaultExceptionResponse: ExceptionResponse =
new InternalServerErrorException().getResponse() as ExceptionResponse;
private exceptionResponse: ExceptionResponse = this.defaultExceptionResponse;
catch(exception: TypeORMError | QueryFailedError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
exception instanceof QueryFailedError &&
this.setQueryFailedErrorResponse(exception);
response
.status(this.exceptionResponse.statusCode)
.json(this.exceptionResponse);
}
private setQueryFailedErrorResponse(exception: QueryFailedError): void {
const error = exception.driverError;
if (error.code === '23505') {
const message = error.detail.replace(
/^Key \((.*)\)=\((.*)\) (.*)/,
'The $1 $2 already exists.',
);
this.exceptionResponse = {
statusCode: HttpStatus.BAD_REQUEST,
message,
};
}
// Other error codes can be handled here
}
// Add more methods here to set a different response for any other typeORM error, if needed.
// All typeORM erros: https://github.com/typeorm/typeorm/tree/master/src/error
}
I set it globally:
import { TypeORMExceptionFilter } from './common';
async function bootstrap() {
//...Other code
app.useGlobalFilters(new TypeORMExceptionFilter());
//...Other code
await app.listen(3000);
}
bootstrap();
And now I don't have to add any code when doing changes in the database:
#Injectable()
export class MerchantsService {
constructor(
#InjectRepository(Merchant)
private merchantRepository: Repository<Merchant>,
) {}
public async create(createMerchantDto: CreateMerchantDto) {
const user = this.merchantRepository.create({
...createMerchantDto,
documentType: DocumentType.NIT,
isActive: false,
});
await this.merchantRepository.save(user);
const { password, ...merchantData } = createMerchantDto;
return {
...merchantData,
};
}
}
Notice that now I don't use try catch because nest is handling the exceptions. When the repository save() method returns an error (actually it is a rejected promise), it is caught in the filter.

Intercepting in Multer Mutates Request? (NestJS)

Does multer mutates any request that has given to it? I'm currently trying to intercept the request to add this in logs.
But whenever I try to execute this code first:
const newReq = cloneDeep(request); // lodash cloneDeep
const newRes = cloneDeep(response);
const postMulterRequest: any = await new Promise((resolve, reject) => {
const multerReponse = multer().any()
multerReponse(request, newRes, err => {
if (err) reject(err)
resolve(request)
})
})
files = postMulterRequest?.files;
The #UseInterceptors(FileInterceptor('file')) becomes undefined.
I have already seen the problem, it seems like the multerReponse(request, newRes, err => { mutates the request. But I don't know what the other approach I can do to fix this. (I tried JSON Serialization, Object.assign, cloneDeep, but none of those worked)
I have tried adding newReq and newRes (cloned object) to multerResponse at first it worked. But at the second time, the thread only hangs up, and doesn't proceed to next steps. Or the multerReponse(request, newRes, err => { doesn't return anything.
The whole code looks like this and used globally (some parts of here were redacted/removed; but the main logic is still the same) :
#Injectable()
export class AuditingInterceptor implements NestInterceptor {
constructor(
#InjectModel(Auditing.name)
private readonly AuditingModel: Model<Auditing>,
) {}
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
const { headers, method, ip, route, query, body } = request;
let bodyParam = Object.assign({}, body),
files: any;
const newReq = cloneDeep(request); // lodash cloneDeep
const newRes = cloneDeep(response);
const postMulterRequest: any = await new Promise((resolve, reject) => {
const multerReponse = multer().any();
multerReponse(newReq, newRes, (err) => {
if (err) reject(err);
resolve(newReq);
});
});
files = postMulterRequest?.files;
return next.handle().pipe(
tap(() =>
this.AuditingModel.create({
request: {
query,
bodyParam,
files,
},
timeAccessed: new Date().toISOString(),
}),
),
);
}
}
Summary of what I need to do here is I need to intercept and log the file in our DB before it gets processed in the method/endpoint that uses #UseInterceptors(FileInterceptor('file')).
I have solve this by intercepting the request using the
#Req() req
and creating a method to handle the files that was intercepted inside the FileInterceptor decorator.
Code Example:
// create logs service first to handle your queries
createLogs(file, req){
// do what you need to do with the file, and req here
const { filename } = file;
const { ip } = req
....
}
// main service
// inject the service first
constructor(#Inject(LogsService) private logsService: LogsService)
uploadHandler(file, req){
this.logsService.createLogs(file, req)
// proceed with the next steps
....
}
// controller
#Post('upload')
#UseInterceptors(FileInterceptor('file'))
testFunction(#UploadedFile() file: Express.Multer.File,, #Req req){
return this.serviceNameHere.uploadHandler(file, req);
}

I need suggestion in saving logs in NestJS

I need suggestion about how to properly or most efficient way to save user logs in database.
So I want to log every time the user do CRUD. I want to save the
userId
event or the action, if he did he remove or update
the Json response of the api
and also the request response of the api.
what I have, in user service layer I call another service name auditService which insert the logs in database.
async create(createUserDto: CreateUserDto): Promise<User> {
try {
const user = new User();
user.email = createUserDto.email;
user.role = createUserDto.role;
user.firstName = createUserDto.firstName;
user.lastName = createUserDto.lastName;
const rs = await this.usersRepository.save(user);
const audit = new AuditLog();
audit.userId = rs.id;
audit.eventType = CREATE_CUSTOMER_SUCESS;
audit.rqMessage = createUserDto;
audit.rsMessage = rs;
//Audit Service which save the logs.
await this.auditService.create(audit);
return rs;
} catch (err) {
// Error
}
Well, this works. but I know there is more efficient way than this. Thank you.
To have full access over request and Response, the best way is by setting a Logger Interceptor or Middleware.
For example, if you are keeping the log to MongoDB, here is an example:
#Injectable()
export class LoggingInterceptor implements NestInterceptor {
constructor(#InjectModel('Log') private logModel: Model<LogDocument>) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context?.switchToHttp()?.getRequest<Request>();
const { statusCode } = context?.switchToHttp()?.getResponse<Response>();
const { originalUrl, method, params, query, body, headers, user } = req;
const requestTime = new Date();
const request: RequestInterface = {
originalUrl,
method,
params,
query,
body,
headers,
};
return next.handle().pipe(
tap((data) => {
const response = { statusCode, data };
this.insertMongo(originalUrl, request, response, requestTime);
}),
);
}
private async insertMongo(
endpoint: string,
request: RequestInterface,
response: ResponseInterface,
requestTime: Date,
): Promise<LogDocument> {
const logInfo: CreateLogDto = {
endpoint,
request,
response,
requestTime,
};
const createdLog = new this.logModel(logInfo);
return createdLog.save();
}
}
It will handle the Request, Response, Context, and Timestamp of every request intercepted.
To use it in a module, you just have to add an APP_INTERCEPTOR provider to it. In the case of the example logger, it should look like this:
providers: [
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
],

NestJS - how to check if JWT can access specific endpoint

Let's say I have 3 endpoints: A is for generating JWT token, B for accessing data of user X and C for accessing data of user Y. Now, what I want to do is, that I can from recieved token somehow in controller guards figure out, if user can access endpoint.
So, token generated for user X can only access endpoint B, token generated for user Y can only access endpoint C.
Token has to be generated at endpoint A, since users sign in at same form.
If question is unclear ask in comment.
You Can do that by specifying in the payload a role, by this role you set a guard on each endpoint which role has the access to it. let me give an example:
I believe that you have a function where you fill you payload kind of this function :
createJwtPayload(user){
let data: JwtPayload = {
userData: user,
companyId : user.company.id,
role:user.role.name, // for us this where we specify the role for our User
};
......
}
Now We have to create guards we need to specify access for x endpoints
let start with Admin Guard:
#Injectable()
export class AdminGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
if (!request.headers.authorization) {
return false;
}
request.user = await this.validateToken(request.headers.authorization);
if( request.user.role == ROLES.SUPER_ADMIN) {
return true;
}
return false;
}
async validateToken(auth: string) {
......
}
lets make the second guard we call it EmployeGuard :
....
#Injectable()
export class EmployeGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
if (!request.headers.authorization) {
return false;
}
request.user = await this.validateToken(request.headers.authorization);
if( request.user.role == ROLES.COMPANY_ADMIN || request.user.role == ROLES.USER) {
return true;
}
return false;
}
async validateToken(auth: string) {
......
}
Now to use these guards we just need to use #UseGuards() in our endpoint :
#Post()
#UseGuards(AdminGuard)
async addCompany(#Res() res, #Body() createDto: CompanyDto) {
........
}
#Get(':companyID')
#UseGuards(EmployeGuard)
async getcompany(#Res() res, #Param('companyID') companyID) {
....
}
Bonus: you can #useGuards on the controller to make sure the all endpoints use it

Resources